diff options
author | Agustin Zubiaga <aguz@sugarlabs.org> | 2012-07-02 00:42:05 (GMT) |
---|---|---|
committer | Agustin Zubiaga <aguz@sugarlabs.org> | 2012-07-02 00:42:05 (GMT) |
commit | 1a133902641f5e81425fe330877f0c2b2c9fc17a (patch) | |
tree | 453bade1c6d60d0544955412a5ecd1102bbaf22c /pycha | |
parent | 02a15b015a905ec2f3329a2935114502a4c08711 (diff) |
Using sugar-pycha
Diffstat (limited to 'pycha')
-rw-r--r-- | pycha/__init__.py | 18 | ||||
-rw-r--r-- | pycha/bar.py | 319 | ||||
-rw-r--r-- | pycha/chart.py | 883 | ||||
-rw-r--r-- | pycha/color.py | 204 | ||||
-rw-r--r-- | pycha/line.py | 130 | ||||
-rw-r--r-- | pycha/pie.py | 352 | ||||
-rw-r--r-- | pycha/polygonal.py | 372 | ||||
-rw-r--r-- | pycha/radial.py | 346 | ||||
-rw-r--r-- | pycha/scatter.py | 38 | ||||
-rw-r--r-- | pycha/stackedbar.py | 121 | ||||
-rw-r--r-- | pycha/utils.py | 39 |
11 files changed, 0 insertions, 2822 deletions
diff --git a/pycha/__init__.py b/pycha/__init__.py deleted file mode 100644 index 35bba09..0000000 --- a/pycha/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com> -# -# 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 <http://www.gnu.org/licenses/>. - -version = "0.6.1dev" diff --git a/pycha/bar.py b/pycha/bar.py deleted file mode 100644 index 8a3168a..0000000 --- a/pycha/bar.py +++ /dev/null @@ -1,319 +0,0 @@ -# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com> -# -# 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 <http://www.gnu.org/licenses/>. - -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 - yerr = 0.0 - - 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 ("<pycha.bar.Rect@(%.2f, %.2f) %.2fx%.2f (%.2f, %.2f, %.2f) %s>" - % (self.x, self.y, self.w, self.h, - self.xval, self.yval, self.yerr, - self.name)) diff --git a/pycha/chart.py b/pycha/chart.py deleted file mode 100644 index 5be11cd..0000000 --- a/pycha/chart.py +++ /dev/null @@ -1,883 +0,0 @@ -# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com> -# -# 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 <http://www.gnu.org/licenses/>. - -import copy -import inspect -import math - -import cairo - -from pycha.color import ColorScheme, hex2rgb, DEFAULT_COLOR -from pycha.utils import safe_unicode - - -class Chart(object): - - def __init__(self, surface, options={}, debug=False): - # this flag is useful to reuse this chart for drawing different data - # or use different options - self.resetFlag = False - - # initialize storage - self.datasets = [] - - # computed values used in several methods - self.layout = Layout() - self.minxval = None - self.maxxval = None - self.minyval = None - self.maxyval = None - self.xscale = 1.0 - self.yscale = 1.0 - self.xrange = None - self.yrange = None - self.origin = 0.0 - - self.xticks = [] - self.yticks = [] - - # set the default options - self.options = copy.deepcopy(DEFAULT_OPTIONS) - if options: - self.options.merge(options) - - # initialize the surface - self._initSurface(surface) - - self.colorScheme = None - - # debug mode to draw aditional hints - self.debug = debug - - def addDataset(self, dataset): - """Adds an object containing chart data to the storage hash""" - self.datasets += dataset - - def _getDatasetsKeys(self): - """Return the name of each data set""" - return [d[0] for d in self.datasets] - - def _getDatasetsValues(self): - """Return the data (value) of each data set""" - return [d[1] for d in self.datasets] - - def setOptions(self, options={}): - """Sets options of this chart""" - self.options.merge(options) - - def getSurfaceSize(self): - cx = cairo.Context(self.surface) - x, y, w, h = cx.clip_extents() - return w, h - - def reset(self): - """Resets options and datasets. - - In the next render the surface will be cleaned before any drawing. - """ - self.resetFlag = True - self.options = copy.deepcopy(DEFAULT_OPTIONS) - self.datasets = [] - - def render(self, surface=None, options={}): - """Renders the chart with the specified options. - - The optional parameters can be used to render a chart in a different - surface with new options. - """ - self._update(options) - if surface: - self._initSurface(surface) - - cx = cairo.Context(self.surface) - - # calculate area data - surface_width, surface_height = self.getSurfaceSize() - self.layout.update(cx, self.options, surface_width, surface_height, - self.xticks, self.yticks) - - self._renderBackground(cx) - if self.debug: - self.layout.render(cx) - self._renderChart(cx) - self._renderAxis(cx) - self._renderTitle(cx) - self._renderLegend(cx) - - def clean(self): - """Clears the surface with a white background.""" - cx = cairo.Context(self.surface) - cx.save() - cx.set_source_rgb(1, 1, 1) - cx.paint() - cx.restore() - - def _setColorscheme(self): - """Sets the colorScheme used for the chart using the - options.colorScheme option - """ - name = self.options.colorScheme.name - keys = self._getDatasetsKeys() - colorSchemeClass = ColorScheme.getColorScheme(name, None) - if colorSchemeClass is None: - raise ValueError('Color scheme "%s" is invalid!' % name) - - # Remove invalid args before calling the constructor - kwargs = dict(self.options.colorScheme.args) - validArgs = inspect.getargspec(colorSchemeClass.__init__)[0] - kwargs = dict([(k, v) for k, v in kwargs.items() if k in validArgs]) - self.colorScheme = colorSchemeClass(keys, **kwargs) - - def _initSurface(self, surface): - self.surface = surface - - if self.resetFlag: - self.resetFlag = False - self.clean() - - def _update(self, options={}): - """Update all the information needed to render the chart""" - self.setOptions(options) - self._setColorscheme() - self._updateXY() - self._updateChart() - self._updateTicks() - - def _updateXY(self): - """Calculates all kinds of metrics for the x and y axis""" - x_range_is_defined = self.options.axis.x.range is not None - y_range_is_defined = self.options.axis.y.range is not None - - if not x_range_is_defined or not y_range_is_defined: - stores = self._getDatasetsValues() - - # gather data for the x axis - if x_range_is_defined: - self.minxval, self.maxxval = self.options.axis.x.range - else: - xdata = [pair[0] for pair in reduce(lambda a, b: a+b, stores)] - self.minxval = float(min(xdata)) - self.maxxval = float(max(xdata)) - if self.minxval * self.maxxval > 0 and self.minxval > 0: - self.minxval = 0.0 - - self.xrange = self.maxxval - self.minxval - if self.xrange == 0: - self.xscale = 1.0 - else: - self.xscale = 1.0 / self.xrange - - # gather data for the y axis - if y_range_is_defined: - self.minyval, self.maxyval = self.options.axis.y.range - else: - ydata = [pair[1] for pair in reduce(lambda a, b: a+b, stores)] - self.minyval = float(min(ydata)) - self.maxyval = float(max(ydata)) - if self.minyval * self.maxyval > 0 and self.minyval > 0: - self.minyval = 0.0 - - self.yrange = self.maxyval - self.minyval - if self.yrange == 0: - self.yscale = 1.0 - else: - self.yscale = 1.0 / self.yrange - - if self.minyval * self.maxyval < 0: # different signs - self.origin = abs(self.minyval) * self.yscale - else: - self.origin = 0.0 - - def _updateChart(self): - raise NotImplementedError - - def _updateTicks(self): - """Evaluates ticks for x and y axis. - - You should call _updateXY before because that method computes the - values of xscale, minxval, yscale, and other attributes needed for - this method. - """ - stores = self._getDatasetsValues() - - # evaluate xTicks - self.xticks = [] - if self.options.axis.x.ticks: - for tick in self.options.axis.x.ticks: - if not isinstance(tick, Option): - tick = Option(tick) - if tick.label is None: - label = str(tick.v) - else: - label = tick.label - pos = self.xscale * (tick.v - self.minxval) - if 0.0 <= pos <= 1.0: - self.xticks.append((pos, label)) - - elif self.options.axis.x.interval > 0: - interval = self.options.axis.x.interval - label = (divmod(self.minxval, interval)[0] + 1) * interval - pos = self.xscale * (label - self.minxval) - prec = self.options.axis.x.tickPrecision - while 0.0 <= pos <= 1.0: - pretty_label = round(label, prec) - if prec == 0: - pretty_label = int(pretty_label) - self.xticks.append((pos, pretty_label)) - label += interval - pos = self.xscale * (label - self.minxval) - - elif self.options.axis.x.tickCount > 0: - uniqx = range(len(uniqueIndices(stores)) + 1) - roughSeparation = self.xrange / self.options.axis.x.tickCount - i = j = 0 - while i < len(uniqx) and j < self.options.axis.x.tickCount: - if (uniqx[i] - self.minxval) >= (j * roughSeparation): - pos = self.xscale * (uniqx[i] - self.minxval) - if 0.0 <= pos <= 1.0: - self.xticks.append((pos, uniqx[i])) - j += 1 - i += 1 - - # evaluate yTicks - self.yticks = [] - if self.options.axis.y.ticks: - for tick in self.options.axis.y.ticks: - if not isinstance(tick, Option): - tick = Option(tick) - if tick.label is None: - label = str(tick.v) - else: - label = tick.label - pos = 1.0 - (self.yscale * (tick.v - self.minyval)) - if 0.0 <= pos <= 1.0: - self.yticks.append((pos, label)) - - elif self.options.axis.y.interval > 0: - interval = self.options.axis.y.interval - label = (divmod(self.minyval, interval)[0] + 1) * interval - pos = 1.0 - (self.yscale * (label - self.minyval)) - prec = self.options.axis.y.tickPrecision - while 0.0 <= pos <= 1.0: - pretty_label = round(label, prec) - if prec == 0: - pretty_label = int(pretty_label) - self.yticks.append((pos, pretty_label)) - label += interval - pos = 1.0 - (self.yscale * (label - self.minyval)) - - elif self.options.axis.y.tickCount > 0: - prec = self.options.axis.y.tickPrecision - num = self.yrange / self.options.axis.y.tickCount - if (num < 1 and prec == 0): - roughSeparation = 1 - else: - roughSeparation = round(num, prec) - - for i in range(self.options.axis.y.tickCount + 1): - yval = self.minyval + (i * roughSeparation) - pos = 1.0 - ((yval - self.minyval) * self.yscale) - if 0.0 <= pos <= 1.0: - pretty_label = round(yval, prec) - if prec == 0: - pretty_label = int(pretty_label) - self.yticks.append((pos, pretty_label)) - - def _renderBackground(self, cx): - """Renders the background area of the chart""" - if self.options.background.hide: - return - - cx.save() - - if self.options.background.baseColor: - cx.set_source_rgb(*hex2rgb(self.options.background.baseColor)) - cx.paint() - - if self.options.background.chartColor: - cx.set_source_rgb(*hex2rgb(self.options.background.chartColor)) - surface_width, surface_height = self.getSurfaceSize() - cx.rectangle(self.options.padding.left, self.options.padding.top, - surface_width - (self.options.padding.left - + self.options.padding.right), - surface_height - (self.options.padding.top - + self.options.padding.bottom)) - cx.fill() - - if self.options.background.lineColor: - cx.set_source_rgb(*hex2rgb(self.options.background.lineColor)) - cx.set_line_width(self.options.axis.lineWidth) - self._renderLines(cx) - - cx.restore() - - def _renderLines(self, cx): - """Aux function for _renderBackground""" - if self.options.axis.y.showLines and self.yticks: - for tick in self.yticks: - self._renderLine(cx, tick, False) - if self.options.axis.x.showLines and self.xticks: - for tick in self.xticks: - self._renderLine(cx, tick, True) - - def _renderLine(self, cx, tick, horiz): - """Aux function for _renderLines""" - x1, x2, y1, y2 = (0, 0, 0, 0) - if horiz: - x1 = x2 = tick[0] * self.layout.chart.w + self.layout.chart.x - y1 = self.layout.chart.y - y2 = y1 + self.layout.chart.h - else: - x1 = self.layout.chart.x - x2 = x1 + self.layout.chart.w - y1 = y2 = tick[0] * self.layout.chart.h + self.layout.chart.y - - cx.new_path() - cx.move_to(x1, y1) - cx.line_to(x2, y2) - cx.close_path() - cx.stroke() - - def _renderChart(self, cx): - raise NotImplementedError - - def _renderTick(self, cx, tick, x, y, x2, y2, rotate, text_position): - """Aux method for _renderXTick and _renderYTick""" - if callable(tick): - return - - cx.new_path() - cx.move_to(x, y) - cx.line_to(x2, y2) - cx.close_path() - cx.stroke() - - cx.select_font_face(self.options.axis.tickFont, - cairo.FONT_SLANT_NORMAL, - cairo.FONT_WEIGHT_NORMAL) - cx.set_font_size(self.options.axis.tickFontSize) - - label = safe_unicode(tick[1], self.options.encoding) - xb, yb, width, height, xa, ya = cx.text_extents(label) - - x, y = text_position - - if rotate: - cx.save() - cx.translate(x, y) - cx.rotate(math.radians(rotate)) - x = -width / 2.0 - y = -height / 2.0 - cx.move_to(x - xb, y - yb) - cx.show_text(label) - if self.debug: - cx.rectangle(x, y, width, height) - cx.stroke() - cx.restore() - else: - x -= width / 2.0 - y -= height / 2.0 - cx.move_to(x - xb, y - yb) - cx.show_text(label) - if self.debug: - cx.rectangle(x, y, width, height) - cx.stroke() - - return label - - def _renderYTick(self, cx, tick): - """Aux method for _renderAxis""" - x = self.layout.y_ticks.x + self.layout.y_ticks.w - y = self.layout.y_ticks.y + tick[0] * self.layout.y_ticks.h - - text_position = ((self.layout.y_tick_labels.x - + self.layout.y_tick_labels.w / 2.0), y) - - return self._renderTick(cx, tick, - x, y, - x - self.options.axis.tickSize, y, - self.options.axis.y.rotate, - text_position) - - def _renderXTick(self, cx, tick): - """Aux method for _renderAxis""" - - x = self.layout.x_ticks.x + tick[0] * self.layout.x_ticks.w - y = self.layout.x_ticks.y - - text_position = (x, (self.layout.x_tick_labels.y - + self.layout.x_tick_labels.h / 2.0)) - - return self._renderTick(cx, tick, - x, y, - x, y + self.options.axis.tickSize, - self.options.axis.x.rotate, - text_position) - - def _renderAxisLabel(self, cx, label, x, y, vertical=False): - cx.save() - cx.select_font_face(self.options.axis.labelFont, - cairo.FONT_SLANT_NORMAL, - cairo.FONT_WEIGHT_BOLD) - cx.set_font_size(self.options.axis.labelFontSize) - cx.set_source_rgb(*hex2rgb(self.options.axis.labelColor)) - - xb, yb, width, height, xa, ya = cx.text_extents(label) - - if vertical: - y = y + width / 2.0 - cx.move_to(x - xb, y - yb) - cx.translate(x, y) - cx.rotate(-math.radians(90)) - cx.move_to(-xb, -yb) - cx.show_text(label) - if self.debug: - cx.rectangle(0, 0, width, height) - cx.stroke() - else: - x = x - width / 2.0 - cx.move_to(x - xb, y - yb) - cx.show_text(label) - if self.debug: - cx.rectangle(x, y, width, height) - cx.stroke() - cx.restore() - - def _renderYAxisLabel(self, cx, label_text): - label = safe_unicode(label_text, self.options.encoding) - x = self.layout.y_label.x - y = self.layout.y_label.y + self.layout.y_label.h / 2.0 - self._renderAxisLabel(cx, label, x, y, True) - - def _renderYAxis(self, cx): - """Draws the vertical line represeting the Y axis""" - cx.new_path() - cx.move_to(self.layout.chart.x, self.layout.chart.y) - cx.line_to(self.layout.chart.x, - self.layout.chart.y + self.layout.chart.h) - cx.close_path() - cx.stroke() - - def _renderXAxisLabel(self, cx, label_text): - label = safe_unicode(label_text, self.options.encoding) - x = self.layout.x_label.x + self.layout.x_label.w / 2.0 - y = self.layout.x_label.y - self._renderAxisLabel(cx, label, x, y, False) - - def _renderXAxis(self, cx): - """Draws the horizontal line representing the X axis""" - cx.new_path() - y = self.layout.chart.y + (1.0 - self.origin) * self.layout.chart.h - cx.move_to(self.layout.chart.x, y) - cx.line_to(self.layout.chart.x + self.layout.chart.w, y) - cx.close_path() - cx.stroke() - - def _renderAxis(self, cx): - """Renders axis""" - if self.options.axis.x.hide and self.options.axis.y.hide: - return - - cx.save() - cx.set_source_rgb(*hex2rgb(self.options.axis.lineColor)) - cx.set_line_width(self.options.axis.lineWidth) - - if not self.options.axis.y.hide: - if self.yticks: - for tick in self.yticks: - self._renderYTick(cx, tick) - - if self.options.axis.y.label: - self._renderYAxisLabel(cx, self.options.axis.y.label) - - self._renderYAxis(cx) - - if not self.options.axis.x.hide: - if self.xticks: - for tick in self.xticks: - self._renderXTick(cx, tick) - - if self.options.axis.x.label: - self._renderXAxisLabel(cx, self.options.axis.x.label) - - self._renderXAxis(cx) - - cx.restore() - - def _renderTitle(self, cx): - if self.options.title: - cx.save() - cx.select_font_face(self.options.titleFont, - cairo.FONT_SLANT_NORMAL, - cairo.FONT_WEIGHT_BOLD) - cx.set_font_size(self.options.titleFontSize) - cx.set_source_rgb(*hex2rgb(self.options.titleColor)) - - title = safe_unicode(self.options.title, self.options.encoding) - extents = cx.text_extents(title) - title_width = extents[2] - - x = (self.layout.title.x - + self.layout.title.w / 2.0 - - title_width / 2.0) - y = self.layout.title.y - extents[1] - - cx.move_to(x, y) - cx.show_text(title) - - cx.restore() - - def _renderLegend(self, cx): - """This function adds a legend to the chart""" - if self.options.legend.hide: - return - - surface_width, surface_height = self.getSurfaceSize() - - # Compute legend dimensions - padding = 4 - bullet = 15 - width = 0 - height = padding - keys = self._getDatasetsKeys() - cx.select_font_face(self.options.legend.legendFont, - cairo.FONT_SLANT_NORMAL, - cairo.FONT_WEIGHT_NORMAL) - cx.set_font_size(self.options.legend.legendFontSize) - for key in keys: - key = safe_unicode(key, self.options.encoding) - extents = cx.text_extents(key) - width = max(extents[2], width) - height += max(extents[3], bullet) + padding - width = padding + bullet + padding + width + padding - - # Compute legend position - legend = self.options.legend - if legend.position.right is not None: - legend.position.left = (surface_width - - legend.position.right - - width) - if legend.position.bottom is not None: - legend.position.top = (surface_height - - legend.position.bottom - - height) - - # Draw the legend - cx.save() - cx.rectangle(self.options.legend.position.left, - self.options.legend.position.top, - width, height) - cx.set_source_rgba(1, 1, 1, self.options.legend.opacity) - cx.fill_preserve() - cx.set_line_width(self.options.legend.borderWidth) - cx.set_source_rgb(*hex2rgb(self.options.legend.borderColor)) - cx.stroke() - - def drawKey(key, x, y, text_height): - cx.rectangle(x, y, bullet, bullet) - cx.set_source_rgb(*self.colorScheme[key]) - cx.fill_preserve() - cx.set_source_rgb(0, 0, 0) - cx.stroke() - cx.move_to(x + bullet + padding, - y + bullet / 2.0 + text_height / 2.0) - cx.show_text(key) - - cx.set_line_width(1) - x = self.options.legend.position.left + padding - y = self.options.legend.position.top + padding - for key in keys: - extents = cx.text_extents(key) - drawKey(key, x, y, extents[3]) - y += max(extents[3], bullet) + padding - - cx.restore() - - -def uniqueIndices(arr): - """Return a list with the indexes of the biggest element of arr""" - return range(max([len(a) for a in arr])) - - -class Area(object): - """Simple rectangle to hold an area coordinates and dimensions""" - - def __init__(self, x=0.0, y=0.0, w=0.0, h=0.0): - self.x, self.y, self.w, self.h = x, y, w, h - - def __str__(self): - msg = "<pycha.chart.Area@(%.2f, %.2f) %.2f x %.2f>" - return msg % (self.x, self.y, self.w, self.h) - - -def get_text_extents(cx, text, font, font_size, encoding): - if text: - cx.save() - cx.select_font_face(font, - cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_BOLD) - cx.set_font_size(font_size) - safe_text = safe_unicode(text, encoding) - extents = cx.text_extents(safe_text) - cx.restore() - return extents[2:4] - return (0.0, 0.0) - - -class Layout(object): - """Set of chart areas""" - - def __init__(self): - self.title = Area() - self.x_label = Area() - self.y_label = Area() - self.x_tick_labels = Area() - self.y_tick_labels = Area() - self.x_ticks = Area() - self.y_ticks = Area() - self.chart = Area() - - self._areas = ( - (self.title, (1, 126/255.0, 0)), # orange - (self.y_label, (41/255.0, 91/255.0, 41/255.0)), # grey - (self.x_label, (41/255.0, 91/255.0, 41/255.0)), # grey - (self.y_tick_labels, (0, 115/255.0, 0)), # green - (self.x_tick_labels, (0, 115/255.0, 0)), # green - (self.y_ticks, (229/255.0, 241/255.0, 18/255.0)), # yellow - (self.x_ticks, (229/255.0, 241/255.0, 18/255.0)), # yellow - (self.chart, (75/255.0, 75/255.0, 1.0)), # blue - ) - - def update(self, cx, options, width, height, xticks, yticks): - self.title.x = options.padding.left - self.title.y = options.padding.top - self.title.w = width - (options.padding.left + options.padding.right) - self.title.h = get_text_extents(cx, - options.title, - options.titleFont, - options.titleFontSize, - options.encoding)[1] - x_axis_label_height = get_text_extents(cx, - options.axis.x.label, - options.axis.labelFont, - options.axis.labelFontSize, - options.encoding)[1] - y_axis_label_width = get_text_extents(cx, - options.axis.y.label, - options.axis.labelFont, - options.axis.labelFontSize, - options.encoding)[1] - - x_axis_tick_labels_height = self._getAxisTickLabelsSize(cx, options, - options.axis.x, - xticks)[1] - y_axis_tick_labels_width = self._getAxisTickLabelsSize(cx, options, - options.axis.y, - yticks)[0] - - self.y_label.x = options.padding.left - self.y_label.y = options.padding.top + self.title.h - self.y_label.w = y_axis_label_width - self.y_label.h = height - (options.padding.bottom - + options.padding.top - + x_axis_label_height - + x_axis_tick_labels_height - + options.axis.tickSize - + self.title.h) - self.x_label.x = (options.padding.left - + y_axis_label_width - + y_axis_tick_labels_width - + options.axis.tickSize) - self.x_label.y = height - (options.padding.bottom - + x_axis_label_height) - self.x_label.w = width - (options.padding.left - + options.padding.right - + options.axis.tickSize - + y_axis_label_width - + y_axis_tick_labels_width) - self.x_label.h = x_axis_label_height - - self.y_tick_labels.x = self.y_label.x + self.y_label.w - self.y_tick_labels.y = self.y_label.y - self.y_tick_labels.w = y_axis_tick_labels_width - self.y_tick_labels.h = self.y_label.h - - self.x_tick_labels.x = self.x_label.x - self.x_tick_labels.y = self.x_label.y - x_axis_tick_labels_height - self.x_tick_labels.w = self.x_label.w - self.x_tick_labels.h = x_axis_tick_labels_height - - self.y_ticks.x = self.y_tick_labels.x + self.y_tick_labels.w - self.y_ticks.y = self.y_tick_labels.y - self.y_ticks.w = options.axis.tickSize - self.y_ticks.h = self.y_label.h - - self.x_ticks.x = self.x_tick_labels.x - self.x_ticks.y = self.x_tick_labels.y - options.axis.tickSize - self.x_ticks.w = self.x_label.w - self.x_ticks.h = options.axis.tickSize - - self.chart.x = self.y_ticks.x + self.y_ticks.w - self.chart.y = self.title.y + self.title.h - self.chart.w = self.x_ticks.w - self.chart.h = self.y_ticks.h - - def render(self, cx): - - def draw_area(area, r, g, b): - cx.rectangle(area.x, area.y, area.w, area.h) - cx.set_source_rgba(r, g, b, 0.5) - cx.fill() - - cx.save() - for area, color in self._areas: - draw_area(area, *color) - cx.restore() - - def _getAxisTickLabelsSize(self, cx, options, axis, ticks): - cx.save() - cx.select_font_face(options.axis.tickFont, - cairo.FONT_SLANT_NORMAL, - cairo.FONT_WEIGHT_NORMAL) - cx.set_font_size(options.axis.tickFontSize) - - max_width = max_height = 0.0 - if not axis.hide: - extents = [cx.text_extents(safe_unicode( - tick[1], options.encoding, - ))[2:4] # get width and height as a tuple - for tick in ticks] - if extents: - widths, heights = zip(*extents) - max_width, max_height = max(widths), max(heights) - if axis.rotate: - radians = math.radians(axis.rotate) - sin = math.sin(radians) - cos = math.cos(radians) - max_width, max_height = ( - max_width * cos + max_height * sin, - max_width * sin + max_height * cos, - ) - cx.restore() - return max_width, max_height - - -class Option(dict): - """Useful dict that allow attribute-like access to its keys""" - - def __getattr__(self, name): - if name in self.keys(): - return self[name] - else: - raise AttributeError(name) - - def merge(self, other): - """Recursive merge with other Option or dict object""" - for key, value in other.items(): - if key in self: - if isinstance(self[key], Option): - self[key].merge(other[key]) - else: - self[key] = other[key] - - -DEFAULT_OPTIONS = Option( - axis=Option( - lineWidth=1.0, - lineColor='#0f0000', - tickSize=3.0, - labelColor='#666666', - labelFont='Tahoma', - labelFontSize=9, - tickFont='Tahoma', - tickFontSize=9, - x=Option( - hide=False, - ticks=None, - tickCount=10, - tickPrecision=1, - range=None, - rotate=None, - label=None, - interval=0, - showLines=False, - ), - y=Option( - hide=False, - ticks=None, - tickCount=10, - tickPrecision=1, - range=None, - rotate=None, - label=None, - interval=0, - showLines=True, - ), - ), - background=Option( - hide=False, - baseColor=None, - chartColor='#f5f5f5', - lineColor='#ffffff', - lineWidth=1.5, - ), - legend=Option( - opacity=0.8, - borderColor='#000000', - borderWidth=2, - hide=False, - legendFont='Tahoma', - legendFontSize=9, - position=Option(top=20, left=40, bottom=None, right=None), - ), - padding=Option( - left=10, - right=10, - top=10, - bottom=10, - ), - stroke=Option( - color='#ffffff', - hide=False, - shadow=True, - width=2 - ), - yvals=Option( - show=False, - inside=False, - fontSize=11, - fontColor='#000000', - skipSmallValues=True, - snapToOrigin=False, - renderer=None - ), - fillOpacity=1.0, - shouldFill=True, - barWidthFillFraction=0.75, - pieRadius=0.4, - colorScheme=Option( - name='gradient', - args=Option( - initialColor=DEFAULT_COLOR, - colors=None, - ), - ), - title=None, - titleColor='#000000', - titleFont='Tahoma', - titleFontSize=12, - encoding='utf-8', -) diff --git a/pycha/color.py b/pycha/color.py deleted file mode 100644 index b01e0e1..0000000 --- a/pycha/color.py +++ /dev/null @@ -1,204 +0,0 @@ -# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com> -# 2009 by Yaco S.L. <lgs@yaco.es> -# -# 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 <http://www.gnu.org/licenses/>. - -import math - -from pycha.utils import clamp - - -DEFAULT_COLOR = '#3c581a' - - -def hex2rgb(hexstring, digits=2): - """Converts a hexstring color to a rgb tuple. - - Example: #ff0000 -> (1.0, 0.0, 0.0) - - digits is an integer number telling how many characters should be - interpreted for each component in the hexstring. - """ - if isinstance(hexstring, (tuple, list)): - return hexstring - - top = float(int(digits * 'f', 16)) - r = int(hexstring[1:digits+1], 16) - g = int(hexstring[digits+1:digits*2+1], 16) - b = int(hexstring[digits*2+1:digits*3+1], 16) - return r / top, g / top, b / top - - -def rgb2hsv(r, g, b): - """Converts a RGB color into a HSV one - - See http://en.wikipedia.org/wiki/HSV_color_space - """ - maximum = max(r, g, b) - minimum = min(r, g, b) - if maximum == minimum: - h = 0.0 - elif maximum == r: - h = 60.0 * ((g - b) / (maximum - minimum)) + 360.0 - if h >= 360.0: - h -= 360.0 - elif maximum == g: - h = 60.0 * ((b - r) / (maximum - minimum)) + 120.0 - elif maximum == b: - h = 60.0 * ((r - g) / (maximum - minimum)) + 240.0 - - if maximum == 0.0: - s = 0.0 - else: - s = 1.0 - (minimum / maximum) - - v = maximum - - return h, s, v - - -def hsv2rgb(h, s, v): - """Converts a HSV color into a RGB one - - See http://en.wikipedia.org/wiki/HSV_color_space - """ - hi = int(math.floor(h / 60.0)) % 6 - f = (h / 60.0) - hi - p = v * (1 - s) - q = v * (1 - f * s) - t = v * (1 - (1 - f) * s) - - if hi == 0: - r, g, b = v, t, p - elif hi == 1: - r, g, b = q, v, p - elif hi == 2: - r, g, b = p, v, t - elif hi == 3: - r, g, b = p, q, v - elif hi == 4: - r, g, b = t, p, v - elif hi == 5: - r, g, b = v, p, q - - return r, g, b - - -def lighten(r, g, b, amount): - """Return a lighter version of the color (r, g, b)""" - return (clamp(0.0, 1.0, r + amount), - clamp(0.0, 1.0, g + amount), - clamp(0.0, 1.0, b + amount)) - - -basicColors = dict( - red='#6d1d1d', - green=DEFAULT_COLOR, - blue='#224565', - grey='#444444', - black='#000000', - darkcyan='#305755', - ) - - -class ColorSchemeMetaclass(type): - """This metaclass is used to autoregister all ColorScheme classes""" - - def __new__(mcs, name, bases, dict): - klass = type.__new__(mcs, name, bases, dict) - klass.registerColorScheme() - return klass - - -class ColorScheme(dict): - """A color scheme is a dictionary where the keys match the keys - constructor argument and the values are colors""" - - __metaclass__ = ColorSchemeMetaclass - __registry__ = {} - - def __init__(self, keys): - super(ColorScheme, self).__init__() - - @classmethod - def registerColorScheme(cls): - key = cls.__name__.replace('ColorScheme', '').lower() - if key: - cls.__registry__[key] = cls - - @classmethod - def getColorScheme(cls, name, default=None): - return cls.__registry__.get(name, default) - - -class GradientColorScheme(ColorScheme): - """In this color scheme each color is a lighter version of initialColor. - - This difference is computed based on the number of keys. - - The initialColor is given in a hex string format. - """ - - def __init__(self, keys, initialColor=DEFAULT_COLOR): - super(GradientColorScheme, self).__init__(keys) - if initialColor in basicColors: - initialColor = basicColors[initialColor] - - r, g, b = hex2rgb(initialColor) - light = 1.0 / (len(keys) * 2) - - for i, key in enumerate(keys): - self[key] = lighten(r, g, b, light * i) - - -class FixedColorScheme(ColorScheme): - """In this color scheme fixed colors are used. - - These colors are provided as a list argument in the constructor. - """ - - def __init__(self, keys, colors=[]): - super(FixedColorScheme, self).__init__(keys) - - if len(keys) != len(colors): - raise ValueError("You must provide as many colors as datasets " - "for the fixed color scheme") - - for i, key in enumerate(keys): - self[key] = hex2rgb(colors[i]) - - -class RainbowColorScheme(ColorScheme): - """In this color scheme the rainbow is divided in N pieces - where N is the number of datasets. - - So each dataset gets a color of the rainbow. - """ - - def __init__(self, keys, initialColor=DEFAULT_COLOR): - super(RainbowColorScheme, self).__init__(keys) - if initialColor in basicColors: - initialColor = basicColors[initialColor] - - r, g, b = hex2rgb(initialColor) - h, s, v = rgb2hsv(r, g, b) - - angleDelta = 360.0 / (len(keys) + 1) - for key in keys: - self[key] = hsv2rgb(h, s, v) - h += angleDelta - if h >= 360.0: - h -= 360.0 diff --git a/pycha/line.py b/pycha/line.py deleted file mode 100644 index 71116b1..0000000 --- a/pycha/line.py +++ /dev/null @@ -1,130 +0,0 @@ -# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com> -# -# 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 <http://www.gnu.org/licenses/>. - -from pycha.chart import Chart -from pycha.color import hex2rgb - - -class LineChart(Chart): - - def __init__(self, surface=None, options={}, debug=False): - super(LineChart, self).__init__(surface, options, debug) - self.points = [] - - def _updateChart(self): - """Evaluates measures for line charts""" - self.points = [] - - for i, (name, store) in enumerate(self.datasets): - for item in store: - xval, yval = item - x = (xval - self.minxval) * self.xscale - y = 1.0 - (yval - self.minyval) * self.yscale - point = Point(x, y, xval, yval, name) - - if 0.0 <= point.x <= 1.0 and 0.0 <= point.y <= 1.0: - self.points.append(point) - - def _renderChart(self, cx): - """Renders a line chart""" - - def preparePath(storeName): - cx.new_path() - firstPoint = True - lastX = None - if self.options.shouldFill: - # Go to the (0,0) coordinate to start drawing the area - #cx.move_to(self.layout.chart.x, - # self.layout.chart.y + self.layout.chart.h) - offset = (1.0 - self.origin) * self.layout.chart.h - cx.move_to(self.layout.chart.x, self.layout.chart.y + offset) - - for point in self.points: - if point.name == storeName: - if not self.options.shouldFill and firstPoint: - # starts the first point of the line - cx.move_to(point.x * self.layout.chart.w - + self.layout.chart.x, - point.y * self.layout.chart.h - + self.layout.chart.y) - firstPoint = False - continue - cx.line_to(point.x * self.layout.chart.w - + self.layout.chart.x, - point.y * self.layout.chart.h - + self.layout.chart.y) - # we remember the last X coordinate to close the area - # properly. See bug #4 - lastX = point.x - - if self.options.shouldFill: - # Close the path to the start point - y = ((1.0 - self.origin) * self.layout.chart.h - + self.layout.chart.y) - cx.line_to(lastX * self.layout.chart.w - + self.layout.chart.x, y) - cx.line_to(self.layout.chart.x, y) - cx.close_path() - else: - cx.set_source_rgb(*self.colorScheme[storeName]) - cx.stroke() - - - cx.save() - cx.set_line_width(self.options.stroke.width) - if self.options.shouldFill: - - def drawLine(storeName): - if self.options.stroke.shadow: - # draw shadow - cx.save() - cx.set_source_rgba(0, 0, 0, 0.15) - cx.translate(2, -2) - preparePath(storeName) - cx.fill() - cx.restore() - - # fill the line - cx.set_source_rgb(*self.colorScheme[storeName]) - preparePath(storeName) - cx.fill() - - if not self.options.stroke.hide: - # draw stroke - cx.set_source_rgb(*hex2rgb(self.options.stroke.color)) - preparePath(storeName) - cx.stroke() - - # draw the lines - for key in self._getDatasetsKeys(): - drawLine(key) - else: - for key in self._getDatasetsKeys(): - preparePath(key) - - cx.restore() - - -class Point(object): - - def __init__(self, x, y, xval, yval, name): - self.x, self.y = x, y - self.xval, self.yval = xval, yval - self.name = name - - def __str__(self): - return "<pycha.line.Point@(%.2f, %.2f)>" % (self.x, self.y) diff --git a/pycha/pie.py b/pycha/pie.py deleted file mode 100644 index 9585c37..0000000 --- a/pycha/pie.py +++ /dev/null @@ -1,352 +0,0 @@ -# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com> -# -# 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 <http://www.gnu.org/licenses/>. - -import math - -import cairo - -from pycha.chart import Chart, Option, Layout, Area, get_text_extents -from pycha.color import hex2rgb - - -class PieChart(Chart): - - def __init__(self, surface=None, options={}, debug=False): - super(PieChart, self).__init__(surface, options, debug) - self.slices = [] - self.centerx = 0 - self.centery = 0 - self.layout = PieLayout(self.slices) - - def _updateChart(self): - """Evaluates measures for pie charts""" - slices = [dict(name=key, - value=(i, value[0][1])) - for i, (key, value) in enumerate(self.datasets)] - - s = float(sum([slice['value'][1] for slice in slices])) - - fraction = angle = 0.0 - - del self.slices[:] - for slice in slices: - if slice['value'][1] > 0: - angle += fraction - fraction = slice['value'][1] / s - self.slices.append(Slice(slice['name'], fraction, - slice['value'][0], slice['value'][1], - angle)) - - def _updateTicks(self): - """Evaluates pie ticks""" - self.xticks = [] - if self.options.axis.x.ticks: - lookup = dict([(slice.xval, slice) for slice in self.slices]) - for tick in self.options.axis.x.ticks: - if not isinstance(tick, Option): - tick = Option(tick) - slice = lookup.get(tick.v, None) - label = tick.label or str(tick.v) - if slice is not None: - label += ' (%.1f%%)' % (slice.fraction * 100) - self.xticks.append((tick.v, label)) - else: - for slice in self.slices: - label = '%s (%.1f%%)' % (slice.name, slice.fraction * 100) - self.xticks.append((slice.xval, label)) - - def _renderLines(self, cx): - """Aux function for _renderBackground""" - # there are no lines in a Pie Chart - - def _renderChart(self, cx): - """Renders a pie chart""" - self.centerx = self.layout.chart.x + self.layout.chart.w * 0.5 - self.centery = self.layout.chart.y + self.layout.chart.h * 0.5 - - cx.set_line_join(cairo.LINE_JOIN_ROUND) - - if self.options.stroke.shadow and False: - cx.save() - cx.set_source_rgba(0, 0, 0, 0.15) - - cx.new_path() - cx.move_to(self.centerx, self.centery) - cx.arc(self.centerx + 1, self.centery + 2, - self.layout.radius + 1, 0, math.pi * 2) - cx.line_to(self.centerx, self.centery) - cx.close_path() - cx.fill() - cx.restore() - - cx.save() - for slice in self.slices: - if slice.isBigEnough(): - cx.set_source_rgb(*self.colorScheme[slice.name]) - if self.options.shouldFill: - slice.draw(cx, self.centerx, self.centery, - self.layout.radius) - cx.fill() - - if not self.options.stroke.hide: - slice.draw(cx, self.centerx, self.centery, - self.layout.radius) - cx.set_line_width(self.options.stroke.width) - cx.set_source_rgb(*hex2rgb(self.options.stroke.color)) - cx.stroke() - - cx.restore() - - if self.debug: - cx.set_source_rgba(1, 0, 0, 0.5) - px = max(cx.device_to_user_distance(1, 1)) - for x, y in self.layout._lines: - cx.arc(x, y, 5 * px, 0, 2 * math.pi) - cx.fill() - cx.new_path() - cx.move_to(self.centerx, self.centery) - cx.line_to(x, y) - cx.stroke() - - def _renderAxis(self, cx): - """Renders the axis for pie charts""" - if self.options.axis.x.hide or not self.xticks: - return - - self.xlabels = [] - - if self.debug: - px = max(cx.device_to_user_distance(1, 1)) - cx.set_source_rgba(0, 0, 1, 0.5) - for x, y, w, h in self.layout.ticks: - cx.rectangle(x, y, w, h) - cx.stroke() - cx.arc(x + w / 2.0, y + h / 2.0, 5 * px, 0, 2 * math.pi) - cx.fill() - cx.arc(x, y, 2 * px, 0, 2 * math.pi) - cx.fill() - - cx.select_font_face(self.options.axis.tickFont, - cairo.FONT_SLANT_NORMAL, - cairo.FONT_WEIGHT_NORMAL) - cx.set_font_size(self.options.axis.tickFontSize) - - cx.set_source_rgb(*hex2rgb(self.options.axis.labelColor)) - - for i, tick in enumerate(self.xticks): - label = tick[1] - x, y, w, h = self.layout.ticks[i] - - xb, yb, width, height, xa, ya = cx.text_extents(label) - - # draw label with text tick[1] - cx.move_to(x - xb, y - yb) - cx.show_text(label) - self.xlabels.append(label) - - -class Slice(object): - - def __init__(self, name, fraction, xval, yval, angle): - self.name = name - self.fraction = fraction - self.xval = xval - self.yval = yval - self.startAngle = 2 * angle * math.pi - self.endAngle = 2 * (angle + fraction) * math.pi - - def __str__(self): - return ("<pycha.pie.Slice from %.2f to %.2f (%.2f%%)>" % - (self.startAngle, self.endAngle, self.fraction)) - - def isBigEnough(self): - return abs(self.startAngle - self.endAngle) > 0.001 - - def draw(self, cx, centerx, centery, radius): - cx.new_path() - cx.move_to(centerx, centery) - cx.arc(centerx, centery, radius, -self.endAngle, -self.startAngle) - cx.close_path() - - def getNormalisedAngle(self): - normalisedAngle = (self.startAngle + self.endAngle) / 2 - - if normalisedAngle > math.pi * 2: - normalisedAngle -= math.pi * 2 - elif normalisedAngle < 0: - normalisedAngle += math.pi * 2 - - return normalisedAngle - - -class PieLayout(Layout): - """Set of chart areas for pie charts""" - - def __init__(self, slices): - self.slices = slices - - self.title = Area() - self.chart = Area() - - self.ticks = [] - self.radius = 0 - - self._areas = ( - (self.title, (1, 126 / 255.0, 0)), # orange - (self.chart, (75 / 255.0, 75 / 255.0, 1.0)), # blue - ) - - self._lines = [] - - def update(self, cx, options, width, height, xticks, yticks): - self.title.x = options.padding.left - self.title.y = options.padding.top - self.title.w = width - (options.padding.left + options.padding.right) - self.title.h = get_text_extents(cx, - options.title, - options.titleFont, - options.titleFontSize, - options.encoding)[1] - - lookup = dict([(slice.xval, slice) for slice in self.slices]) - - self.chart.x = self.title.x - self.chart.y = self.title.y + self.title.h - self.chart.w = self.title.w - self.chart.h = height - self.title.h - (options.padding.top - + options.padding.bottom) - - centerx = self.chart.x + self.chart.w * 0.5 - centery = self.chart.y + self.chart.h * 0.5 - - self.radius = min(self.chart.w / 2.0, self.chart.h / 2.0) - for tick in xticks: - slice = lookup.get(tick[0], None) - width, height = get_text_extents(cx, tick[1], - options.axis.tickFont, - options.axis.tickFontSize, - options.encoding) - angle = slice.getNormalisedAngle() - radius = self._get_min_radius(angle, centerx, centery, - width, height) - self.radius = min(self.radius, radius) - - # Now that we now the radius we move the ticks as close as we can - # to the circle - for i, tick in enumerate(xticks): - slice = lookup.get(tick[0], None) - angle = slice.getNormalisedAngle() - self.ticks[i] = self._get_tick_position(self.radius, angle, - self.ticks[i], - centerx, centery) - - def _get_min_radius(self, angle, centerx, centery, width, height): - min_radius = None - - # precompute some common values - tan = math.tan(angle) - half_width = width / 2.0 - half_height = height / 2.0 - offset_x = half_width * tan - offset_y = half_height / tan - - def intersect_horizontal_line(y): - return centerx + (centery - y) / tan - - def intersect_vertical_line(x): - return centery - tan * (x - centerx) - - # computes the intersection between the rect that has - # that angle with the X axis and the bounding chart box - if 0.25 * math.pi <= angle < 0.75 * math.pi: - # intersects with the top rect - y = self.chart.y - x = intersect_horizontal_line(y) - self._lines.append((x, y)) - - x1 = x - half_width - offset_y - self.ticks.append((x1, self.chart.y, width, height)) - - min_radius = abs((y + height) - centery) - elif 0.75 * math.pi <= angle < 1.25 * math.pi: - # intersects with the left rect - x = self.chart.x - y = intersect_vertical_line(x) - self._lines.append((x, y)) - - y1 = y - half_height - offset_x - self.ticks.append((x, y1, width, height)) - - min_radius = abs(centerx - (x + width)) - elif 1.25 * math.pi <= angle < 1.75 * math.pi: - # intersects with the bottom rect - y = self.chart.y + self.chart.h - x = intersect_horizontal_line(y) - self._lines.append((x, y)) - - x1 = x - half_width + offset_y - self.ticks.append((x1, y - height, width, height)) - - min_radius = abs((y - height) - centery) - else: - # intersects with the right rect - x = self.chart.x + self.chart.w - y = intersect_vertical_line(x) - self._lines.append((x, y)) - - y1 = y - half_height + offset_x - self.ticks.append((x - width, y1, width, height)) - - min_radius = abs((x - width) - centerx) - - return min_radius - - def _get_tick_position(self, radius, angle, tick, centerx, centery): - text_width, text_height = tick[2:4] - half_width = text_width / 2.0 - half_height = text_height / 2.0 - - if 0 <= angle < 0.5 * math.pi: - # first quadrant - k1 = j1 = k2 = 1 - j2 = -1 - elif 0.5 * math.pi <= angle < math.pi: - # second quadrant - k1 = k2 = -1 - j1 = j2 = 1 - elif math.pi <= angle < 1.5 * math.pi: - # third quadrant - k1 = j1 = k2 = -1 - j2 = 1 - elif 1.5 * math.pi <= angle < 2 * math.pi: - # fourth quadrant - k1 = k2 = 1 - j1 = j2 = -1 - - cx = radius * math.cos(angle) + k1 * half_width - cy = radius * math.sin(angle) + j1 * half_height - - radius2 = math.sqrt(cx * cx + cy * cy) - - tan = math.tan(angle) - x = math.sqrt((radius2 * radius2) / (1 + tan * tan)) - y = tan * x - - x = centerx + k2 * x - y = centery + j2 * y - - return x - half_width, y - half_height, text_width, text_height diff --git a/pycha/polygonal.py b/pycha/polygonal.py deleted file mode 100644 index b470c87..0000000 --- a/pycha/polygonal.py +++ /dev/null @@ -1,372 +0,0 @@ -# Copyright(c) 2011 by Roberto Garcia Carvajal <roberpot@gmail.com> -# -# 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 <http://www.gnu.org/licenses/>. - -import math - -import cairo - -from pycha.chart import Chart -from pycha.line import Point -from pycha.color import hex2rgb -from pycha.utils import safe_unicode - - -class PolygonalChart(Chart): - - def __init__(self, surface=None, options={}): - super(PolygonalChart, self).__init__(surface, options) - self.points = [] - - def _updateChart(self): - """Evaluates measures for polygonal charts""" - self.points = [] - - for i, (name, store) in enumerate(self.datasets): - for item in store: - xval, yval = item - x = (xval - self.minxval) * self.xscale - y = 1.0 - (yval - self.minyval) * self.yscale - point = Point(x, y, xval, yval, name) - - if 0.0 <= point.x <= 1.0 and 0.0 <= point.y <= 1.0: - self.points.append(point) - - def _renderBackground(self, cx): - """Renders the background area of the chart""" - if self.options.background.hide: - return - - cx.save() - - if self.options.background.baseColor: - cx.set_source_rgb(*hex2rgb(self.options.background.baseColor)) - cx.paint() - - if self.options.background.chartColor: - cx.set_source_rgb(*hex2rgb(self.options.background.chartColor)) - cx.set_line_width(10.0) - cx.new_path() - init = None - count = len(self.xticks) - for index, tick in enumerate(self.xticks): - ang = math.pi / 2 - index * 2 * math.pi / count - x = (self.layout.chart.x + self.layout.chart.w / 2 - - math.cos(ang) - * min(self.layout.chart.w / 2, self.layout.chart.h / 2)) - y = (self.layout.chart.y + self.layout.chart.h / 2 - - math.sin(ang) - * min(self.layout.chart.w / 2, self.layout.chart.h / 2)) - if init is None: - cx.move_to(x, y) - init = (x, y) - else: - cx.line_to(x, y) - cx.line_to(init[0], init[1]) - cx.close_path() - cx.fill() - - if self.options.background.lineColor: - cx.set_source_rgb(*hex2rgb(self.options.background.lineColor)) - cx.set_line_width(self.options.axis.lineWidth) - self._renderLines(cx) - - cx.restore() - - def _renderLine(self, cx, tick, horiz): - """Aux function for _renderLines""" - - rad = (self.layout.chart.h / 2) * (1 - tick[0]) - cx.new_path() - init = None - count = len(self.xticks) - for index, tick in enumerate(self.xticks): - ang = math.pi / 2 - index * 2 * math.pi / count - x = (self.layout.chart.x + self.layout.chart.w / 2 - - math.cos(ang) * rad) - y = (self.layout.chart.y + self.layout.chart.h / 2 - - math.sin(ang) * rad) - if init is None: - cx.move_to(x, y) - init = (x, y) - else: - cx.line_to(x, y) - cx.line_to(init[0], init[1]) - cx.close_path() - cx.stroke() - - def _renderXAxis(self, cx): - """Draws the horizontal line representing the X axis""" - - count = len(self.xticks) - - centerx = self.layout.chart.x + self.layout.chart.w / 2 - centery = self.layout.chart.y + self.layout.chart.h / 2 - - for i in range(0, count): - offset1 = i * 2 * math.pi / count - offset = math.pi / 2 - offset1 - - rad = self.layout.chart.h / 2 - (r1, r2) = (0, rad + 5) - - x1 = centerx - math.cos(offset) * r1 - x2 = centerx - math.cos(offset) * r2 - y1 = centery - math.sin(offset) * r1 - y2 = centery - math.sin(offset) * r2 - - cx.new_path() - cx.move_to(x1, y1) - cx.line_to(x2, y2) - cx.close_path() - cx.stroke() - - def _renderYTick(self, cx, tick, center): - """Aux method for _renderAxis""" - - i = tick - tick = self.yticks[i] - - count = len(self.yticks) - - if callable(tick): - return - - x = center[0] - y = center[1] - i * (self.layout.chart.h / 2) / count - - cx.new_path() - cx.move_to(x, y) - cx.line_to(x - self.options.axis.tickSize, y) - cx.close_path() - cx.stroke() - - cx.select_font_face(self.options.axis.tickFont, - cairo.FONT_SLANT_NORMAL, - cairo.FONT_WEIGHT_NORMAL) - cx.set_font_size(self.options.axis.tickFontSize) - - label = safe_unicode(tick[1], self.options.encoding) - extents = cx.text_extents(label) - labelWidth = extents[2] - labelHeight = extents[3] - - if self.options.axis.y.rotate: - radians = math.radians(self.options.axis.y.rotate) - cx.move_to(x - self.options.axis.tickSize - - (labelWidth * math.cos(radians)) - - 4, - y + (labelWidth * math.sin(radians)) - + labelHeight / (2.0 / math.cos(radians))) - cx.rotate(-radians) - cx.show_text(label) - cx.rotate(radians) # this is probably faster than a save/restore - else: - cx.move_to(x - self.options.axis.tickSize - labelWidth - 4, - y + labelHeight / 2.0) - cx.rel_move_to(0.0, -labelHeight / 2.0) - cx.show_text(label) - - return label - - def _renderYAxis(self, cx): - """Draws the vertical line for the Y axis""" - - centerx = self.layout.chart.x + self.layout.chart.w / 2 - centery = self.layout.chart.y + self.layout.chart.h / 2 - - offset = math.pi / 2 - - r1 = self.layout.chart.h / 2 - - x1 = centerx - math.cos(offset) * r1 - y1 = centery - math.sin(offset) * r1 - - cx.new_path() - cx.move_to(centerx, centery) - cx.line_to(x1, y1) - cx.close_path() - cx.stroke() - - def _renderAxis(self, cx): - """Renders axis""" - if self.options.axis.x.hide and self.options.axis.y.hide: - return - - cx.save() - cx.set_source_rgb(*hex2rgb(self.options.axis.lineColor)) - cx.set_line_width(self.options.axis.lineWidth) - - centerx = self.layout.chart.x + self.layout.chart.w / 2 - centery = self.layout.chart.y + self.layout.chart.h / 2 - - if not self.options.axis.y.hide: - if self.yticks: - - count = len(self.yticks) - - for i in range(0, count): - self._renderYTick(cx, i, (centerx, centery)) - - if self.options.axis.y.label: - self._renderYAxisLabel(cx, self.options.axis.y.label) - - self._renderYAxis(cx) - - if not self.options.axis.x.hide: - fontAscent = cx.font_extents()[0] - if self.xticks: - - count = len(self.xticks) - - for i in range(0, count): - self._renderXTick(cx, i, fontAscent, (centerx, centery)) - - if self.options.axis.x.label: - self._renderXAxisLabel(cx, self.options.axis.x.label) - - self._renderXAxis(cx) - - cx.restore() - - def _renderXTick(self, cx, i, fontAscent, center): - tick = self.xticks[i] - if callable(tick): - return - - count = len(self.xticks) - cx.select_font_face(self.options.axis.tickFont, - cairo.FONT_SLANT_NORMAL, - cairo.FONT_WEIGHT_NORMAL) - cx.set_font_size(self.options.axis.tickFontSize) - - label = safe_unicode(tick[1], self.options.encoding) - extents = cx.text_extents(label) - labelWidth = extents[2] - labelHeight = extents[3] - - x, y = center - cx.move_to(x, y) - - if self.options.axis.x.rotate: - radians = math.radians(self.options.axis.x.rotate) - cx.move_to(x - (labelHeight * math.cos(radians)), - y + self.options.axis.tickSize - + (labelHeight * math.cos(radians)) - + 4.0) - cx.rotate(radians) - cx.show_text(label) - cx.rotate(-radians) - else: - offset1 = i * 2 * math.pi / count - offset = math.pi / 2 - offset1 - - rad = self.layout.chart.h / 2 + 10 - - x = center[0] - math.cos(offset) * rad - y = center[1] - math.sin(offset) * rad - - cx.move_to(x, y) - cx.rotate(offset - math.pi / 2) - - if math.sin(offset) < 0.0: - cx.rotate(math.pi) - cx.rel_move_to(0.0, 5.0) - - cx.rel_move_to(-labelWidth / 2.0, 0) - cx.show_text(label) - if math.sin(offset) < 0.0: - cx.rotate(-math.pi) - - cx.rotate(-(offset - math.pi / 2)) - return label - - def _renderChart(self, cx): - """Renders a polygonal chart""" - # draw the polygon. - def preparePath(storeName): - cx.new_path() - firstPoint = True - - count = len(self.points) / len(self.datasets) - centerx = self.layout.chart.x + self.layout.chart.w / 2 - centery = self.layout.chart.y + self.layout.chart.h / 2 - - firstPointCoord = None - - for index, point in enumerate(self.points): - if point.name == storeName: - offset1 = index * 2 * math.pi / count - offset = math.pi / 2 - offset1 - - rad = (self.layout.chart.h / 2) * (1 - point.y) - - x = centerx - math.cos(offset) * rad - y = centery - math.sin(offset) * rad - - if firstPointCoord is None: - firstPointCoord = (x, y) - - if not self.options.shouldFill and firstPoint: - # starts the first point of the line - cx.move_to(x, y) - firstPoint = False - continue - cx.line_to(x, y) - - if not firstPointCoord is None: - cx.line_to(firstPointCoord[0], firstPointCoord[1]) - - if self.options.shouldFill: - # Close the path to the start point - y = ((1.0 - self.origin) - * self.layout.chart.h + self.layout.chart.y) - else: - cx.set_source_rgb(*self.colorScheme[storeName]) - cx.stroke() - - cx.save() - cx.set_line_width(self.options.stroke.width) - if self.options.shouldFill: - - def drawLine(storeName): - if self.options.stroke.shadow: - # draw shadow - cx.save() - cx.set_source_rgba(0, 0, 0, 0.15) - cx.translate(2, -2) - preparePath(storeName) - cx.fill() - cx.restore() - - # fill the line - cx.set_source_rgb(*self.colorScheme[storeName]) - preparePath(storeName) - cx.fill() - - if not self.options.stroke.hide: - # draw stroke - cx.set_source_rgb(*hex2rgb(self.options.stroke.color)) - preparePath(storeName) - cx.stroke() - - # draw the lines - for key in self._getDatasetsKeys(): - drawLine(key) - else: - for key in self._getDatasetsKeys(): - preparePath(key) - cx.restore() diff --git a/pycha/radial.py b/pycha/radial.py deleted file mode 100644 index 9055e26..0000000 --- a/pycha/radial.py +++ /dev/null @@ -1,346 +0,0 @@ -# Copyright(c) 2011 by Roberto Garcia Carvajal <roberpot@gmail.com> -# -# 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 <http://www.gnu.org/licenses/>. - -import math - -import cairo - -from pycha.chart import Chart -from pycha.line import Point -from pycha.color import hex2rgb -from pycha.utils import safe_unicode - - -class RadialChart(Chart): - - def __init__(self, surface=None, options={}): - super(RadialChart, self).__init__(surface, options) - self.points = [] - - def _updateChart(self): - """Evaluates measures for radial charts""" - self.points = [] - - for i, (name, store) in enumerate(self.datasets): - for item in store: - xval, yval = item - x = (xval - self.minxval) * self.xscale - y = 1.0 - (yval - self.minyval) * self.yscale - point = Point(x, y, xval, yval, name) - - if 0.0 <= point.x <= 1.0 and 0.0 <= point.y <= 1.0: - self.points.append(point) - - def _renderBackground(self, cx): - """Renders the background area of the chart""" - if self.options.background.hide: - return - - cx.save() - - if self.options.background.baseColor: - cx.set_source_rgb(*hex2rgb(self.options.background.baseColor)) - cx.paint() - - if self.options.background.chartColor: - cx.set_source_rgb(*hex2rgb(self.options.background.chartColor)) - cx.set_line_width(10.0) - cx.arc(self.layout.chart.x + self.layout.chart.w / 2, - self.layout.chart.y + self.layout.chart.h / 2, - min(self.layout.chart.w / 2, self.layout.chart.h / 2), - 0, 2 * math.pi) - cx.fill() - - if self.options.background.lineColor: - cx.set_source_rgb(*hex2rgb(self.options.background.lineColor)) - cx.set_line_width(self.options.axis.lineWidth) - self._renderLines(cx) - - cx.restore() - - def _renderLine(self, cx, tick, horiz): - """Aux function for _renderLines""" - - rad = (self.layout.chart.h / 2) * (1 - tick[0]) - cx.arc(self.layout.chart.x + self.layout.chart.w / 2, - self.layout.chart.y + self.layout.chart.h / 2, - rad, 0, 2 * math.pi) - cx.stroke() - - def _renderXAxis(self, cx): - """Draws the horizontal line representing the X axis""" - - count = len(self.xticks) - - centerx = self.layout.chart.x + self.layout.chart.w / 2 - centery = self.layout.chart.y + self.layout.chart.h / 2 - - for i in range(0, count): - offset1 = i * 2 * math.pi / count - offset = math.pi / 2 - offset1 - - rad = self.layout.chart.h / 2 - (r1, r2) = (0, rad + 5) - - x1 = centerx - math.cos(offset) * r1 - x2 = centerx - math.cos(offset) * r2 - y1 = centery - math.sin(offset) * r1 - y2 = centery - math.sin(offset) * r2 - - cx.new_path() - cx.move_to(x1, y1) - cx.line_to(x2, y2) - cx.close_path() - cx.stroke() - - def _renderYTick(self, cx, tick, center): - """Aux method for _renderAxis""" - - i = tick - tick = self.yticks[i] - - count = len(self.yticks) - - if callable(tick): - return - - x = center[0] - y = center[1] - i * (self.layout.chart.h / 2) / count - - cx.new_path() - cx.move_to(x, y) - cx.line_to(x - self.options.axis.tickSize, y) - cx.close_path() - cx.stroke() - - cx.select_font_face(self.options.axis.tickFont, - cairo.FONT_SLANT_NORMAL, - cairo.FONT_WEIGHT_NORMAL) - cx.set_font_size(self.options.axis.tickFontSize) - - label = safe_unicode(tick[1], self.options.encoding) - extents = cx.text_extents(label) - labelWidth = extents[2] - labelHeight = extents[3] - - if self.options.axis.y.rotate: - radians = math.radians(self.options.axis.y.rotate) - cx.move_to(x - self.options.axis.tickSize - - (labelWidth * math.cos(radians)) - - 4, - y + (labelWidth * math.sin(radians)) - + labelHeight / (2.0 / math.cos(radians))) - cx.rotate(-radians) - cx.show_text(label) - cx.rotate(radians) # this is probably faster than a save/restore - else: - cx.move_to(x - self.options.axis.tickSize - labelWidth - 4, - y + labelHeight / 2.0) - cx.rel_move_to(0.0, -labelHeight / 2.0) - cx.show_text(label) - - return label - - def _renderYAxis(self, cx): - """Draws the vertical line for the Y axis""" - - centerx = self.layout.chart.x + self.layout.chart.w / 2 - centery = self.layout.chart.y + self.layout.chart.h / 2 - - offset = math.pi / 2 - - r1 = self.layout.chart.h / 2 - - x1 = centerx - math.cos(offset) * r1 - y1 = centery - math.sin(offset) * r1 - - cx.new_path() - cx.move_to(centerx, centery) - cx.line_to(x1, y1) - cx.close_path() - cx.stroke() - - def _renderAxis(self, cx): - """Renders axis""" - if self.options.axis.x.hide and self.options.axis.y.hide: - return - - cx.save() - cx.set_source_rgb(*hex2rgb(self.options.axis.lineColor)) - cx.set_line_width(self.options.axis.lineWidth) - - centerx = self.layout.chart.x + self.layout.chart.w / 2 - centery = self.layout.chart.y + self.layout.chart.h / 2 - - if not self.options.axis.y.hide: - if self.yticks: - - count = len(self.yticks) - - for i in range(0, count): - self._renderYTick(cx, i, (centerx, centery)) - - if self.options.axis.y.label: - self._renderYAxisLabel(cx, self.options.axis.y.label) - - self._renderYAxis(cx) - - if not self.options.axis.x.hide: - fontAscent = cx.font_extents()[0] - if self.xticks: - - count = len(self.xticks) - - for i in range(0, count): - self._renderXTick(cx, i, fontAscent, (centerx, centery)) - - if self.options.axis.x.label: - self._renderXAxisLabel(cx, self.options.axis.x.label) - - self._renderXAxis(cx) - - cx.restore() - - def _renderXTick(self, cx, i, fontAscent, center): - tick = self.xticks[i] - if callable(tick): - return - - count = len(self.xticks) - cx.select_font_face(self.options.axis.tickFont, - cairo.FONT_SLANT_NORMAL, - cairo.FONT_WEIGHT_NORMAL) - cx.set_font_size(self.options.axis.tickFontSize) - - label = safe_unicode(tick[1], self.options.encoding) - extents = cx.text_extents(label) - labelWidth = extents[2] - labelHeight = extents[3] - - x, y = center - cx.move_to(x, y) - - if self.options.axis.x.rotate: - radians = math.radians(self.options.axis.x.rotate) - cx.move_to(x - (labelHeight * math.cos(radians)), - y + self.options.axis.tickSize - + (labelHeight * math.cos(radians)) - + 4.0) - cx.rotate(radians) - cx.show_text(label) - cx.rotate(-radians) - else: - offset1 = i * 2 * math.pi / count - offset = math.pi / 2 - offset1 - - rad = self.layout.chart.h / 2 + 10 - - x = center[0] - math.cos(offset) * rad - y = center[1] - math.sin(offset) * rad - - cx.move_to(x, y) - cx.rotate(offset - math.pi / 2) - - if math.sin(offset) < 0.0: - cx.rotate(math.pi) - cx.rel_move_to(0.0, 5.0) - - cx.rel_move_to(-labelWidth / 2.0, 0) - cx.show_text(label) - if math.sin(offset) < 0.0: - cx.rotate(-math.pi) - - cx.rotate(-(offset - math.pi / 2)) - return label - - def _renderChart(self, cx): - """Renders a line chart""" - - # draw the circle - def preparePath(storeName): - cx.new_path() - firstPoint = True - - count = len(self.points) / len(self.datasets) - centerx = self.layout.chart.x + self.layout.chart.w / 2 - centery = self.layout.chart.y + self.layout.chart.h / 2 - - firstPointCoord = None - - for index, point in enumerate(self.points): - if point.name == storeName: - offset1 = index * 2 * math.pi / count - offset = math.pi / 2 - offset1 - - rad = (self.layout.chart.h / 2) * (1 - point.y) - - x = centerx - math.cos(offset) * rad - y = centery - math.sin(offset) * rad - - if firstPointCoord is None: - firstPointCoord = (x, y) - - if not self.options.shouldFill and firstPoint: - # starts the first point of the line - cx.move_to(x, y) - firstPoint = False - continue - cx.line_to(x, y) - - if not firstPointCoord is None: - cx.line_to(firstPointCoord[0], firstPointCoord[1]) - - if self.options.shouldFill: - # Close the path to the start point - y = ((1.0 - self.origin) - * self.layout.chart.h + self.layout.chart.y) - else: - cx.set_source_rgb(*self.colorScheme[storeName]) - cx.stroke() - - cx.save() - cx.set_line_width(self.options.stroke.width) - if self.options.shouldFill: - - def drawLine(storeName): - if self.options.stroke.shadow: - # draw shadow - cx.save() - cx.set_source_rgba(0, 0, 0, 0.15) - cx.translate(2, -2) - preparePath(storeName) - cx.fill() - cx.restore() - - # fill the line - cx.set_source_rgb(*self.colorScheme[storeName]) - preparePath(storeName) - cx.fill() - - if not self.options.stroke.hide: - # draw stroke - cx.set_source_rgb(*hex2rgb(self.options.stroke.color)) - preparePath(storeName) - cx.stroke() - - # draw the lines - for key in self._getDatasetsKeys(): - drawLine(key) - else: - for key in self._getDatasetsKeys(): - preparePath(key) - cx.restore() diff --git a/pycha/scatter.py b/pycha/scatter.py deleted file mode 100644 index 27656de..0000000 --- a/pycha/scatter.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com> -# -# 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 <http://www.gnu.org/licenses/>. - -import math - -from pycha.line import LineChart - - -class ScatterplotChart(LineChart): - - def _renderChart(self, cx): - """Renders a scatterplot""" - - def drawSymbol(point, size): - ox = point.x * self.layout.chart.w + self.layout.chart.x - oy = point.y * self.layout.chart.h + self.layout.chart.y - cx.arc(ox, oy, size, 0.0, 2 * math.pi) - cx.fill() - - for key in self._getDatasetsKeys(): - cx.set_source_rgb(*self.colorScheme[key]) - for point in self.points: - if point.name == key: - drawSymbol(point, self.options.stroke.width) diff --git a/pycha/stackedbar.py b/pycha/stackedbar.py deleted file mode 100644 index 92fdc7f..0000000 --- a/pycha/stackedbar.py +++ /dev/null @@ -1,121 +0,0 @@ -# Copyright(c) 2009 by Yaco S.L. <lgs@yaco.es> -# -# 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 <http://www.gnu.org/licenses/>. - -from pycha.bar import BarChart, VerticalBarChart, HorizontalBarChart, Rect -from pycha.chart import uniqueIndices - - -class StackedBarChart(BarChart): - - def __init__(self, surface=None, options={}, debug=False): - super(StackedBarChart, self).__init__(surface, options, debug) - self.barWidth = 0.0 - - def _updateXY(self): - super(StackedBarChart, 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) - - if self.options.axis.y.range is None: - # Fix the yscale as we accumulate the y values - stores = self._getDatasetsValues() - n_stores = len(stores) - flat_y = [pair[1] for pair in reduce(lambda a, b: a+b, stores)] - store_size = len(flat_y) / n_stores - accum = [sum(flat_y[j]for j in xrange(i, - i + store_size * n_stores, - store_size)) - for i in range(len(flat_y) / n_stores)] - self.yrange = float(max(accum)) - if self.yrange == 0: - self.yscale = 1.0 - else: - self.yscale = 1.0 / self.yrange - - 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 - self.barWidth = k * self.options.barWidthFillFraction - self.barMargin = k * (1.0 - self.options.barWidthFillFraction) / 2 - - self.bars = [] - - -class StackedVerticalBarChart(StackedBarChart, VerticalBarChart): - - def _updateChart(self): - """Evaluates measures for vertical bars""" - super(StackedVerticalBarChart, self)._updateChart() - - accumulated_heights = {} - for i, (name, store) in enumerate(self.datasets): - for item in store: - xval, yval = item - x = ((xval - self.minxval) * self.xscale) + self.barMargin - w = self.barWidth - h = abs(yval) * self.yscale - if yval > 0: - y = (1.0 - h) - self.origin - else: - y = 1 - self.origin - - accumulated_height = accumulated_heights.setdefault(xval, 0) - y -= accumulated_height - accumulated_heights[xval] += h - - 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) - - -class StackedHorizontalBarChart(StackedBarChart, HorizontalBarChart): - - def _updateChart(self): - """Evaluates measures for horizontal bars""" - super(StackedHorizontalBarChart, self)._updateChart() - - accumulated_widths = {} - for i, (name, store) in enumerate(self.datasets): - for item in store: - xval, yval = item - y = ((xval - self.minxval) * self.xscale) + self.barMargin - h = self.barWidth - w = abs(yval) * self.yscale - if yval > 0: - x = self.origin - else: - x = self.origin - w - - accumulated_width = accumulated_widths.setdefault(xval, 0) - x += accumulated_width - accumulated_widths[xval] += w - - 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) diff --git a/pycha/utils.py b/pycha/utils.py deleted file mode 100644 index aefc5b4..0000000 --- a/pycha/utils.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright(c) 2007-2010 by Lorenzo Gil Sanchez <lorenzo.gil.sanchez@gmail.com> -# 2009-2010 by Yaco S.L. <lgs@yaco.es> -# -# 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 <http://www.gnu.org/licenses/>. - -def clamp(minValue, maxValue, value): - """Make sure value is between minValue and maxValue""" - if value < minValue: - return minValue - if value > maxValue: - return maxValue - return value - - -def safe_unicode(obj, encoding=None): - """Return a unicode value from the argument""" - if isinstance(obj, unicode): - return obj - elif isinstance(obj, str): - if encoding is None: - return unicode(obj) - else: - return unicode(obj, encoding) - else: - # it may be an int or a float - return unicode(obj) |