From 6c0355d7194c60dcb778bb34a3df5a9bac511f37 Mon Sep 17 00:00:00 2001 From: Agustin Zubiaga Date: Mon, 02 Jul 2012 00:48:34 +0000 Subject: Some charts.py fixes (sugarpycha) --- (limited to 'sugarpycha/bar.py') diff --git a/sugarpycha/bar.py b/sugarpycha/bar.py new file mode 100644 index 0000000..ab0eeec --- /dev/null +++ b/sugarpycha/bar.py @@ -0,0 +1,318 @@ +# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez +# +# This file is part of PyCha. +# +# PyCha is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PyCha is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with PyCha. If not, see . + +from pycha.chart import Chart, uniqueIndices +from pycha.color import hex2rgb +from pycha.utils import safe_unicode + + +class BarChart(Chart): + + def __init__(self, surface=None, options={}, debug=False): + super(BarChart, self).__init__(surface, options, debug) + self.bars = [] + self.minxdelta = 0.0 + self.barWidthForSet = 0.0 + self.barMargin = 0.0 + + def _updateXY(self): + super(BarChart, self)._updateXY() + # each dataset is centered around a line segment. that's why we + # need n + 1 divisions on the x axis + self.xscale = 1 / (self.xrange + 1.0) + + def _updateChart(self): + """Evaluates measures for vertical bars""" + stores = self._getDatasetsValues() + uniqx = uniqueIndices(stores) + + if len(uniqx) == 1: + self.minxdelta = 1.0 + else: + self.minxdelta = min([abs(uniqx[j] - uniqx[j - 1]) + for j in range(1, len(uniqx))]) + + k = self.minxdelta * self.xscale + barWidth = k * self.options.barWidthFillFraction + self.barWidthForSet = barWidth / len(stores) + self.barMargin = k * (1.0 - self.options.barWidthFillFraction) / 2 + + self.bars = [] + + def _renderChart(self, cx): + """Renders a horizontal/vertical bar chart""" + + def drawBar(bar): + stroke_width = self.options.stroke.width + ux, uy = cx.device_to_user_distance(stroke_width, stroke_width) + if ux < uy: + ux = uy + cx.set_line_width(ux) + + # gather bar proportions + x = self.layout.chart.x + self.layout.chart.w * bar.x + y = self.layout.chart.y + self.layout.chart.h * bar.y + w = self.layout.chart.w * bar.w + h = self.layout.chart.h * bar.h + + if (w < 1 or h < 1) and self.options.yvals.skipSmallValues: + return # don't draw when the bar is too small + + if self.options.stroke.shadow: + cx.set_source_rgba(0, 0, 0, 0.15) + rectangle = self._getShadowRectangle(x, y, w, h) + cx.rectangle(*rectangle) + cx.fill() + + if self.options.shouldFill or (not self.options.stroke.hide): + + if self.options.shouldFill: + cx.set_source_rgb(*self.colorScheme[bar.name]) + cx.rectangle(x, y, w, h) + cx.fill() + + if not self.options.stroke.hide: + cx.set_source_rgb(*hex2rgb(self.options.stroke.color)) + cx.rectangle(x, y, w, h) + cx.stroke() + + if bar.yerr: + self._renderError(cx, x, y, w, h, bar.yval, bar.yerr) + + # render yvals above/beside bars + if self.options.yvals.show: + cx.save() + cx.set_font_size(self.options.yvals.fontSize) + cx.set_source_rgb(*hex2rgb(self.options.yvals.fontColor)) + + if callable(self.options.yvals.renderer): + label = safe_unicode(self.options.yvals.renderer(bar), + self.options.encoding) + else: + label = safe_unicode(bar.yval, self.options.encoding) + extents = cx.text_extents(label) + labelW = extents[2] + labelH = extents[3] + + self._renderYVal(cx, label, labelW, labelH, x, y, w, h) + + cx.restore() + + cx.save() + for bar in self.bars: + drawBar(bar) + cx.restore() + + def _renderYVal(self, cx, label, width, height, x, y, w, h): + raise NotImplementedError + + +class VerticalBarChart(BarChart): + + def _updateChart(self): + """Evaluates measures for vertical bars""" + super(VerticalBarChart, self)._updateChart() + for i, (name, store) in enumerate(self.datasets): + for item in store: + if len(item) == 3: + xval, yval, yerr = item + else: + xval, yval = item + + x = (((xval - self.minxval) * self.xscale) + + self.barMargin + (i * self.barWidthForSet)) + w = self.barWidthForSet + h = abs(yval) * self.yscale + if yval > 0: + y = (1.0 - h) - self.origin + else: + y = 1 - self.origin + rect = Rect(x, y, w, h, xval, yval, name) + + if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0): + self.bars.append(rect) + + def _updateTicks(self): + """Evaluates bar ticks""" + super(BarChart, self)._updateTicks() + offset = (self.minxdelta * self.xscale) / 2 + self.xticks = [(tick[0] + offset, tick[1]) for tick in self.xticks] + + def _getShadowRectangle(self, x, y, w, h): + return (x - 2, y - 2, w + 4, h + 2) + + def _renderYVal(self, cx, label, labelW, labelH, barX, barY, barW, barH): + x = barX + (barW / 2.0) - (labelW / 2.0) + if self.options.yvals.snapToOrigin: + y = barY + barH - 0.5 * labelH + elif self.options.yvals.inside: + y = barY + (1.5 * labelH) + else: + y = barY - 0.5 * labelH + + # if the label doesn't fit below the bar, put it above the bar + if y > (barY + barH): + y = barY - 0.5 * labelH + + cx.move_to(x, y) + cx.show_text(label) + + def _renderError(self, cx, barX, barY, barW, barH, value, error): + center = barX + (barW / 2.0) + errorWidth = max(barW * 0.1, 5.0) + left = center - errorWidth + right = center + errorWidth + errorSize = barH * error / value + top = barY + errorSize + bottom = barY - errorSize + + cx.set_source_rgb(0, 0, 0) + cx.move_to(left, top) + cx.line_to(right, top) + cx.stroke() + cx.move_to(center, top) + cx.line_to(center, bottom) + cx.stroke() + cx.move_to(left, bottom) + cx.line_to(right, bottom) + cx.stroke() + + +class HorizontalBarChart(BarChart): + + def _updateChart(self): + """Evaluates measures for horizontal bars""" + super(HorizontalBarChart, self)._updateChart() + + for i, (name, store) in enumerate(self.datasets): + for item in store: + if len(item) == 3: + xval, yval, yerr = item + else: + xval, yval = item + yerr = 0.0 + + y = (((xval - self.minxval) * self.xscale) + + self.barMargin + (i * self.barWidthForSet)) + h = self.barWidthForSet + w = abs(yval) * self.yscale + if yval > 0: + x = self.origin + else: + x = self.origin - w + rect = Rect(x, y, w, h, xval, yval, name, yerr) + + if (0.0 <= rect.x <= 1.0) and (0.0 <= rect.y <= 1.0): + self.bars.append(rect) + + def _updateTicks(self): + """Evaluates bar ticks""" + super(BarChart, self)._updateTicks() + offset = (self.minxdelta * self.xscale) / 2 + tmp = self.xticks + self.xticks = [(1.0 - tick[0], tick[1]) for tick in self.yticks] + self.yticks = [(tick[0] + offset, tick[1]) for tick in tmp] + + def _renderLines(self, cx): + """Aux function for _renderBackground""" + if self.options.axis.y.showLines and self.yticks: + for tick in self.xticks: + self._renderLine(cx, tick, True) + if self.options.axis.x.showLines and self.xticks: + for tick in self.yticks: + self._renderLine(cx, tick, False) + + def _getShadowRectangle(self, x, y, w, h): + return (x, y - 2, w + 2, h + 4) + + def _renderXAxisLabel(self, cx, labelText): + labelText = self.options.axis.x.label + super(HorizontalBarChart, self)._renderYAxisLabel(cx, labelText) + + def _renderXAxis(self, cx): + """Draws the horizontal line representing the X axis""" + cx.new_path() + cx.move_to(self.layout.chart.x, + self.layout.chart.y + self.layout.chart.h) + cx.line_to(self.layout.chart.x + self.layout.chart.w, + self.layout.chart.y + self.layout.chart.h) + cx.close_path() + cx.stroke() + + def _renderYAxisLabel(self, cx, labelText): + labelText = self.options.axis.y.label + super(HorizontalBarChart, self)._renderXAxisLabel(cx, labelText) + + def _renderYAxis(self, cx): + # draws the vertical line representing the Y axis + cx.new_path() + cx.move_to(self.layout.chart.x + self.origin * self.layout.chart.w, + self.layout.chart.y) + cx.line_to(self.layout.chart.x + self.origin * self.layout.chart.w, + self.layout.chart.y + self.layout.chart.h) + cx.close_path() + cx.stroke() + + def _renderYVal(self, cx, label, labelW, labelH, barX, barY, barW, barH): + y = barY + (barH / 2.0) + (labelH / 2.0) + if self.options.yvals.snapToOrigin: + x = barX + 2 + elif self.options.yvals.inside: + x = barX + barW - (1.2 * labelW) + else: + x = barX + barW + 0.2 * labelW + + # if the label doesn't fit to the left of the bar, put it to the right + if x < barX: + x = barX + barW + 0.2 * labelW + + cx.move_to(x, y) + cx.show_text(label) + + def _renderError(self, cx, barX, barY, barW, barH, value, error): + center = barY + (barH / 2.0) + errorHeight = max(barH * 0.1, 5.0) + top = center + errorHeight + bottom = center - errorHeight + errorSize = barW * error / value + right = barX + barW + errorSize + left = barX + barW - errorSize + + cx.set_source_rgb(0, 0, 0) + cx.move_to(left, top) + cx.line_to(left, bottom) + cx.stroke() + cx.move_to(left, center) + cx.line_to(right, center) + cx.stroke() + cx.move_to(right, top) + cx.line_to(right, bottom) + cx.stroke() + + +class Rect(object): + + def __init__(self, x, y, w, h, xval, yval, name, yerr=0.0): + self.x, self.y, self.w, self.h = x, y, w, h + self.xval, self.yval, self.yerr = xval, yval, yerr + self.name = name + + def __str__(self): + return ("" + % (self.x, self.y, self.w, self.h, + self.xval, self.yval, self.yerr, + self.name)) -- cgit v0.9.1