Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/pycha
diff options
context:
space:
mode:
authorAgustin Zubiaga <aguz@sugarlabs.org>2012-07-02 00:42:05 (GMT)
committer Agustin Zubiaga <aguz@sugarlabs.org>2012-07-02 00:42:05 (GMT)
commit1a133902641f5e81425fe330877f0c2b2c9fc17a (patch)
tree453bade1c6d60d0544955412a5ecd1102bbaf22c /pycha
parent02a15b015a905ec2f3329a2935114502a4c08711 (diff)
Using sugar-pycha
Diffstat (limited to 'pycha')
-rw-r--r--pycha/__init__.py18
-rw-r--r--pycha/bar.py319
-rw-r--r--pycha/chart.py883
-rw-r--r--pycha/color.py204
-rw-r--r--pycha/line.py130
-rw-r--r--pycha/pie.py352
-rw-r--r--pycha/polygonal.py372
-rw-r--r--pycha/radial.py346
-rw-r--r--pycha/scatter.py38
-rw-r--r--pycha/stackedbar.py121
-rw-r--r--pycha/utils.py39
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)