[billreminder] Added pycha charting tool.



commit fa12637e38445e87dd9d9a0e74285db5958b4734
Author: Og B. Maciel <ogmaciel gnome org>
Date:   Tue Nov 24 13:01:58 2009 -0500

    Added pycha charting tool.

 src/gui/widgets/pycha/.svn/all-wcprops             |   53 ++
 src/gui/widgets/pycha/.svn/dir-prop-base           |    6 +
 src/gui/widgets/pycha/.svn/entries                 |  300 ++++++++
 .../pycha/.svn/text-base/__init__.py.svn-base      |   18 +
 .../widgets/pycha/.svn/text-base/bar.py.svn-base   |  243 +++++++
 .../widgets/pycha/.svn/text-base/chart.py.svn-base |  718 ++++++++++++++++++++
 .../widgets/pycha/.svn/text-base/color.py.svn-base |  210 ++++++
 .../widgets/pycha/.svn/text-base/line.py.svn-base  |  123 ++++
 .../widgets/pycha/.svn/text-base/pie.py.svn-base   |  211 ++++++
 .../pycha/.svn/text-base/scatter.py.svn-base       |   54 ++
 .../pycha/.svn/text-base/stackedbar.py.svn-base    |  121 ++++
 src/gui/widgets/pycha/COPYING                      |  165 +++++
 src/gui/widgets/pycha/README.txt                   |  160 +++++
 src/gui/widgets/pycha/__init__.py                  |   18 +
 src/gui/widgets/pycha/bar.py                       |  243 +++++++
 src/gui/widgets/pycha/chart.py                     |  718 ++++++++++++++++++++
 src/gui/widgets/pycha/color.py                     |  210 ++++++
 src/gui/widgets/pycha/line.py                      |  123 ++++
 src/gui/widgets/pycha/pie.py                       |  211 ++++++
 src/gui/widgets/pycha/scatter.py                   |   54 ++
 src/gui/widgets/pycha/stackedbar.py                |  121 ++++
 21 files changed, 4080 insertions(+), 0 deletions(-)
---
diff --git a/src/gui/widgets/pycha/.svn/all-wcprops b/src/gui/widgets/pycha/.svn/all-wcprops
new file mode 100644
index 0000000..7bd10b4
--- /dev/null
+++ b/src/gui/widgets/pycha/.svn/all-wcprops
@@ -0,0 +1,53 @@
+K 25
+svn:wc:ra_dav:version-url
+V 35
+/svn/pycha/!svn/ver/183/trunk/pycha
+END
+pie.py
+K 25
+svn:wc:ra_dav:version-url
+V 42
+/svn/pycha/!svn/ver/175/trunk/pycha/pie.py
+END
+color.py
+K 25
+svn:wc:ra_dav:version-url
+V 44
+/svn/pycha/!svn/ver/171/trunk/pycha/color.py
+END
+__init__.py
+K 25
+svn:wc:ra_dav:version-url
+V 47
+/svn/pycha/!svn/ver/183/trunk/pycha/__init__.py
+END
+chart.py
+K 25
+svn:wc:ra_dav:version-url
+V 44
+/svn/pycha/!svn/ver/175/trunk/pycha/chart.py
+END
+stackedbar.py
+K 25
+svn:wc:ra_dav:version-url
+V 49
+/svn/pycha/!svn/ver/176/trunk/pycha/stackedbar.py
+END
+bar.py
+K 25
+svn:wc:ra_dav:version-url
+V 42
+/svn/pycha/!svn/ver/177/trunk/pycha/bar.py
+END
+scatter.py
+K 25
+svn:wc:ra_dav:version-url
+V 46
+/svn/pycha/!svn/ver/169/trunk/pycha/scatter.py
+END
+line.py
+K 25
+svn:wc:ra_dav:version-url
+V 43
+/svn/pycha/!svn/ver/169/trunk/pycha/line.py
+END
diff --git a/src/gui/widgets/pycha/.svn/dir-prop-base b/src/gui/widgets/pycha/.svn/dir-prop-base
new file mode 100644
index 0000000..4cc643b
--- /dev/null
+++ b/src/gui/widgets/pycha/.svn/dir-prop-base
@@ -0,0 +1,6 @@
+K 10
+svn:ignore
+V 6
+*.pyc
+
+END
diff --git a/src/gui/widgets/pycha/.svn/entries b/src/gui/widgets/pycha/.svn/entries
new file mode 100644
index 0000000..6fee419
--- /dev/null
+++ b/src/gui/widgets/pycha/.svn/entries
@@ -0,0 +1,300 @@
+10
+
+dir
+184
+http://www.lorenzogil.com/svn/pycha/trunk/pycha
+http://www.lorenzogil.com/svn/pycha
+
+
+
+2009-03-22T09:23:45.015465Z
+183
+lgs
+has-props
+
+
+
+
+
+
+
+
+
+
+
+
+
+8b7a50bb-eb0d-4b75-8775-3cf1d405c657
+
+pie.py
+file
+
+
+
+
+2009-10-10T01:47:09.000000Z
+a29e74561a0b01880a208e8f8bb6554b
+2009-03-21T13:19:39.355409Z
+175
+lgs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+7275
+
+color.py
+file
+
+
+
+
+2009-10-10T01:47:09.000000Z
+e182715ca6771e4d02dc336f6acec15b
+2009-03-18T20:37:59.467131Z
+171
+lgs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+5811
+
+__init__.py
+file
+
+
+
+
+2009-10-10T01:47:09.000000Z
+e25f39197f6148f7cd7ac12d1279eac0
+2009-03-22T09:23:45.015465Z
+183
+lgs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+770
+
+chart.py
+file
+
+
+
+
+2009-10-10T01:47:09.000000Z
+93586885361a5885b09c772b481b4e05
+2009-03-21T13:19:39.355409Z
+175
+lgs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+24066
+
+stackedbar.py
+file
+
+
+
+
+2009-10-10T01:47:09.000000Z
+7efe560a5bbd7c3c17c43d2e202f66d9
+2009-03-22T08:38:17.132946Z
+176
+lgs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+4556
+
+bar.py
+file
+
+
+
+
+2009-10-10T01:47:09.000000Z
+6ad74df2d5ac6f826dae1067cda48c45
+2009-03-22T08:54:13.282047Z
+177
+lgs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+8470
+
+scatter.py
+file
+
+
+
+
+2009-10-10T01:47:09.000000Z
+3a593967cf5320eaba52ae639dfc6c78
+2009-03-17T10:27:31.478012Z
+169
+lgs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+1786
+
+line.py
+file
+
+
+
+
+2009-10-10T01:47:09.000000Z
+792a8fa388db881ef01600eff548ce22
+2009-03-17T10:27:31.478012Z
+169
+lgs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+4452
+
diff --git a/src/gui/widgets/pycha/.svn/text-base/__init__.py.svn-base b/src/gui/widgets/pycha/.svn/text-base/__init__.py.svn-base
new file mode 100644
index 0000000..3aabacf
--- /dev/null
+++ b/src/gui/widgets/pycha/.svn/text-base/__init__.py.svn-base
@@ -0,0 +1,18 @@
+# Copyright(c) 2007-2009 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.5.1dev"
diff --git a/src/gui/widgets/pycha/.svn/text-base/bar.py.svn-base b/src/gui/widgets/pycha/.svn/text-base/bar.py.svn-base
new file mode 100644
index 0000000..73fc3d6
--- /dev/null
+++ b/src/gui/widgets/pycha/.svn/text-base/bar.py.svn-base
@@ -0,0 +1,243 @@
+# Copyright(c) 2007-2009 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
+
+
+class BarChart(Chart):
+
+    def __init__(self, surface=None, options={}):
+        super(BarChart, self).__init__(surface, options)
+        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.area.x + self.area.w * bar.x
+            y = self.area.y + self.area.h * bar.y
+            w = self.area.w * bar.w
+            h = self.area.h * bar.h
+
+            if w < 1 or h < 1:
+                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()
+
+            # 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))
+
+                label = unicode(bar.yval)
+                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:
+                xval, yval = item
+                x = (((xval - self.minxval) * self.xscale)
+                    + self.barMargin + (i * self.barWidthForSet))
+                w = self.barWidthForSet
+                h = abs(yval) * self.yscale
+                if yval > 0:
+                    y = (1.0 - h) - self.area.origin
+                else:
+                    y = 1 - self.area.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.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)
+
+
+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:
+                xval, yval = item
+                y = (((xval - self.minxval) * self.xscale)
+                     + self.barMargin + (i * self.barWidthForSet))
+                h = self.barWidthForSet
+                w = abs(yval) * self.yscale
+                if yval > 0:
+                    x = self.area.origin
+                else:
+                    x = self.area.origin - 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)
+
+    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"""
+        ticks = self.xticks
+        for tick in ticks:
+            self._renderLine(cx, tick, True)
+
+    def _getShadowRectangle(self, x, y, w, h):
+        return (x, y-2, w+2, h+4)
+
+    def _renderXAxis(self, cx):
+        """Draws the horizontal line representing the X axis"""
+        cx.new_path()
+        cx.move_to(self.area.x, self.area.y + self.area.h)
+        cx.line_to(self.area.x + self.area.w, self.area.y + self.area.h)
+        cx.close_path()
+        cx.stroke()
+
+    def _renderYAxis(self, cx):
+        # draws the vertical line representing the Y axis
+        cx.new_path()
+        cx.move_to(self.area.x + self.area.origin * self.area.w,
+                   self.area.y)
+        cx.line_to(self.area.x + self.area.origin * self.area.w,
+                   self.area.y + self.area.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.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)
+
+
+class Rect(object):
+
+    def __init__(self, x, y, w, h, xval, yval, name):
+        self.x, self.y, self.w, self.h = x, y, w, h
+        self.xval, self.yval = xval, yval
+        self.name = name
+
+    def __str__(self):
+        return ("<pycha.bar.Rect@(%.2f, %.2f) %.2fx%.2f (%.2f, %.2f) %s>"
+                % (self.x, self.y, self.w, self.h, self.xval, self.yval,
+                   self.name))
diff --git a/src/gui/widgets/pycha/.svn/text-base/chart.py.svn-base b/src/gui/widgets/pycha/.svn/text-base/chart.py.svn-base
new file mode 100644
index 0000000..3d968db
--- /dev/null
+++ b/src/gui/widgets/pycha/.svn/text-base/chart.py.svn-base
@@ -0,0 +1,718 @@
+# Copyright(c) 2007-2009 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
+
+
+class Chart(object):
+
+    def __init__(self, surface, options={}):
+        # 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.area = None # chart area without padding or text labels
+        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.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
+
+    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)
+        self._renderBackground(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
+
+        # calculate area data
+        surface_width, surface_height = self.getSurfaceSize()
+        width = (surface_width
+                 - self.options.padding.left - self.options.padding.right)
+        height = (surface_height
+                  - self.options.padding.top - self.options.padding.bottom)
+
+        if self.minyval * self.maxyval < 0: # different signs
+            origin = abs(self.minyval) * self.yscale
+        else:
+            origin = 0
+
+        self.area = Area(self.options.padding.left,
+                         self.options.padding.top,
+                         width, height, origin)
+
+    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)
+            while 0.0 <= pos <= 1.0:
+                self.xticks.append((pos, 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))
+            while 0.0 <= pos <= 1.0:
+                self.yticks.append((pos, 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:
+                    self.yticks.append((pos, round(yval, prec)))
+
+    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.rectangle(self.area.x, self.area.y, self.area.w, self.area.h)
+            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"""
+        ticks = self.yticks
+        for tick in ticks:
+            self._renderLine(cx, tick, False)
+
+    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.area.w + self.area.x
+            y1 = self.area.y
+            y2 = y1 + self.area.h
+        else:
+            x1 = self.area.x
+            x2 = x1 + self.area.w
+            y1 = y2 = tick[0] * self.area.h + self.area.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 _renderYTick(self, cx, tick):
+        """Aux method for _renderAxis"""
+
+        if callable(tick):
+            return
+
+        x = self.area.x
+        y = self.area.y + tick[0] * self.area.h
+
+        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 = unicode(tick[1])
+        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.show_text(label)
+
+        return label
+
+    def _renderXTick(self, cx, tick, fontAscent):
+        if callable(tick):
+            return
+
+        x = self.area.x + tick[0] * self.area.w
+        y = self.area.y + self.area.h
+
+        cx.new_path()
+        cx.move_to(x, y)
+        cx.line_to(x, y + self.options.axis.tickSize)
+        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 = unicode(tick[1])
+        extents = cx.text_extents(label)
+        labelWidth = extents[2]
+        labelHeight = extents[3]
+
+        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:
+            cx.move_to(x - labelWidth / 2.0,
+                       y + self.options.axis.tickSize
+                       + fontAscent + 4.0)
+            cx.show_text(label)
+        return label
+
+    def _getTickSize(self, cx, ticks, rotate):
+        tickExtents = [cx.text_extents(unicode(tick[1]))[2:4]
+                       for tick in ticks]
+        tickWidth = tickHeight = 0.0
+        if tickExtents:
+            tickHeight = self.options.axis.tickSize + 4.0
+            tickWidth = self.options.axis.tickSize + 4.0
+            widths, heights = zip(*tickExtents)
+            maxWidth, maxHeight = max(widths), max(heights)
+            if rotate:
+                radians = math.radians(rotate)
+                sinRadians = math.sin(radians)
+                cosRadians = math.cos(radians)
+                maxHeight = maxWidth * sinRadians + maxHeight * cosRadians
+                maxWidth = maxWidth * cosRadians + maxHeight * sinRadians
+            tickWidth += maxWidth
+            tickHeight += maxHeight
+        return tickWidth, tickHeight
+
+    def _renderAxisLabel(self, cx, tickWidth, tickHeight, label, x, y,
+                         vertical=False):
+        cx.new_path()
+        cx.select_font_face(self.options.axis.labelFont,
+                            cairo.FONT_SLANT_NORMAL,
+                            cairo.FONT_WEIGHT_BOLD)
+        cx.set_font_size(self.options.axis.labelFontSize)
+        labelWidth = cx.text_extents(label)[2]
+        fontAscent = cx.font_extents()[0]
+        if vertical:
+            cx.move_to(x, y + labelWidth / 2)
+            radians = math.radians(90)
+            cx.rotate(-radians)
+        else:
+            cx.move_to(x - labelWidth / 2.0, y + fontAscent)
+
+        cx.show_text(label)
+
+    def _renderYAxis(self, cx):
+        """Draws the vertical line represeting the Y axis"""
+        cx.new_path()
+        cx.move_to(self.area.x, self.area.y)
+        cx.line_to(self.area.x, self.area.y + self.area.h)
+        cx.close_path()
+        cx.stroke()
+
+    def _renderXAxis(self, cx):
+        """Draws the horizontal line representing the X axis"""
+        cx.new_path()
+        cx.move_to(self.area.x,
+                   self.area.y + self.area.h * (1.0 - self.area.origin))
+        cx.line_to(self.area.x + self.area.w,
+                   self.area.y + self.area.h * (1.0 - self.area.origin))
+        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:
+                cx.save()
+                rotate = self.options.axis.y.rotate
+                tickWidth, tickHeight = self._getTickSize(cx, self.yticks,
+                                                          rotate)
+                label = unicode(self.options.axis.y.label)
+                x = self.area.x - tickWidth - 4.0
+                y = self.area.y + 0.5 * self.area.h
+                self._renderAxisLabel(cx, tickWidth, tickHeight, label, x, y,
+                                      True)
+                cx.restore()
+
+            self._renderYAxis(cx)
+
+        if not self.options.axis.x.hide:
+            fontAscent = cx.font_extents()[0]
+            if self.xticks:
+                for tick in self.xticks:
+                    self._renderXTick(cx, tick, fontAscent)
+
+            if self.options.axis.x.label:
+                cx.save()
+                rotate = self.options.axis.x.rotate
+                tickWidth, tickHeight = self._getTickSize(cx, self.xticks,
+                                                          rotate)
+                label = unicode(self.options.axis.x.label)
+                x = self.area.x + self.area.w / 2.0
+                y = self.area.y + self.area.h + tickHeight + 4.0
+                self._renderAxisLabel(cx, tickWidth, tickHeight, label, x, y,
+                                      False)
+                cx.restore()
+
+            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)
+
+            title = unicode(self.options.title)
+            extents = cx.text_extents(title)
+            titleWidth = extents[2]
+
+            x = self.area.x + self.area.w / 2.0 - titleWidth / 2.0
+            y = cx.font_extents()[0] # font ascent
+
+            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()
+        for key in keys:
+            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.stroke.width)
+        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, y, w, h, origin=0.0):
+        self.x, self.y, self.w, self.h = x, y, w, h
+        self.origin = origin
+
+    def __str__(self):
+        msg = "<pycha.chart.Area@(%.2f, %.2f) %.2f x %.2f Origin: %.2f>"
+        return  msg % (self.x, self.y, self.w, self.h, self.origin)
+
+
+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,
+        labelWidth=50.0,
+        tickFont='Tahoma',
+        tickFontSize=9,
+        x=Option(
+            hide=False,
+            ticks=None,
+            tickCount=10,
+            tickPrecision=1,
+            range=None,
+            rotate=None,
+            label=None,
+            interval=0,
+        ),
+        y=Option(
+            hide=False,
+            ticks=None,
+            tickCount=10,
+            tickPrecision=1,
+            range=None,
+            rotate=None,
+            label=None,
+            interval=0,
+        ),
+    ),
+    background=Option(
+        hide=False,
+        baseColor=None,
+        chartColor='#f5f5f5',
+        lineColor='#ffffff',
+        lineWidth=1.5,
+    ),
+    legend=Option(
+        opacity=0.8,
+        borderColor='#000000',
+        hide=False,
+        position=Option(top=20, left=40, bottom=None, right=None),
+    ),
+    padding=Option(
+        left=30,
+        right=30,
+        top=30,
+        bottom=30,
+    ),
+    stroke=Option(
+        color='#ffffff',
+        hide=False,
+        shadow=True,
+        width=2
+    ),
+    yvals=Option(
+        show=False,
+        inside=False,
+        fontSize=11,
+        fontColor='#000000',
+    ),
+    fillOpacity=1.0,
+    shouldFill=True,
+    barWidthFillFraction=0.75,
+    pieRadius=0.4,
+    colorScheme=Option(
+        name='gradient',
+        args=Option(
+            initialColor=DEFAULT_COLOR,
+            colors=None,
+            ),
+    ),
+    title=None,
+    titleFont='Tahoma',
+    titleFontSize=12,
+)
diff --git a/src/gui/widgets/pycha/.svn/text-base/color.py.svn-base b/src/gui/widgets/pycha/.svn/text-base/color.py.svn-base
new file mode 100644
index 0000000..65c19bd
--- /dev/null
+++ b/src/gui/widgets/pycha/.svn/text-base/color.py.svn-base
@@ -0,0 +1,210 @@
+# Copyright(c) 2007-2009 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
+
+DEFAULT_COLOR = '#3c581a'
+
+
+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 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/src/gui/widgets/pycha/.svn/text-base/line.py.svn-base b/src/gui/widgets/pycha/.svn/text-base/line.py.svn-base
new file mode 100644
index 0000000..2195ee2
--- /dev/null
+++ b/src/gui/widgets/pycha/.svn/text-base/line.py.svn-base
@@ -0,0 +1,123 @@
+# Copyright(c) 2007-2009 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={}):
+        super(LineChart, self).__init__(surface, options)
+        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.area.x, self.area.y + self.area.h)
+                offset = (1.0 - self.area.origin) * self.area.h
+                cx.move_to(self.area.x, self.area.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.area.w + self.area.x,
+                                   point.y * self.area.h + self.area.y)
+                        firstPoint = False
+                        continue
+                    cx.line_to(point.x * self.area.w + self.area.x,
+                               point.y * self.area.h + self.area.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.area.origin) * self.area.h + self.area.y
+                cx.line_to(lastX * self.area.w + self.area.x, y)
+                cx.line_to(self.area.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/src/gui/widgets/pycha/.svn/text-base/pie.py.svn-base b/src/gui/widgets/pycha/.svn/text-base/pie.py.svn-base
new file mode 100644
index 0000000..b0cd381
--- /dev/null
+++ b/src/gui/widgets/pycha/.svn/text-base/pie.py.svn-base
@@ -0,0 +1,211 @@
+# Copyright(c) 2007-2009 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
+from pycha.color import hex2rgb
+
+
+class PieChart(Chart):
+
+    def __init__(self, surface=None, options={}):
+        super(PieChart, self).__init__(surface, options)
+        self.slices = []
+        self.centerx = 0
+        self.centery = 0
+        self.radius = 0
+
+    def _updateChart(self):
+        """Evaluates measures for pie charts"""
+        self.centerx = self.area.x + self.area.w * 0.5
+        self.centery = self.area.y + self.area.h * 0.5
+        self.radius = min(self.area.w * self.options.pieRadius,
+                          self.area.h * self.options.pieRadius)
+
+        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
+
+        self.slices = []
+        for slice in slices:
+            angle += fraction
+            if slice['value'][1] > 0:
+                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 _renderBackground(self, cx):
+        """Renders the background 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))
+            x, y, w, h = 0, 0, self.area.w, self.area.h
+            w += self.options.padding.left + self.options.padding.right
+            h += self.options.padding.top + self.options.padding.bottom
+            cx.rectangle(x, y, w, h)
+            cx.fill()
+
+        cx.restore()
+
+    def _renderChart(self, cx):
+        """Renders a pie chart"""
+        cx.set_line_join(cairo.LINE_JOIN_ROUND)
+
+        if self.options.stroke.shadow:
+            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.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.radius)
+                    cx.fill()
+
+                if not self.options.stroke.hide:
+                    slice.draw(cx, self.centerx, self.centery, self.radius)
+                    cx.set_line_width(self.options.stroke.width)
+                    cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
+                    cx.stroke()
+
+        cx.restore()
+
+    def _renderAxis(self, cx):
+        """Renders the axis for pie charts"""
+        if self.options.axis.x.hide or not self.xticks:
+            return
+
+        self.xlabels = []
+        lookup = dict([(slice.xval, slice) for slice in self.slices])
+
+
+        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 tick in self.xticks:
+            slice = lookup[tick[0]]
+
+            normalisedAngle = slice.getNormalisedAngle()
+
+            big_radius = self.radius + 10
+            labelx = self.centerx + math.sin(normalisedAngle) * big_radius
+            labely = self.centery - math.cos(normalisedAngle) * big_radius
+
+            label = tick[1]
+            extents = cx.text_extents(label)
+            labelWidth = extents[2]
+            labelHeight = extents[3]
+            x = y = 0
+
+            if normalisedAngle <= math.pi * 0.5:
+                x = labelx
+                y = labely - labelHeight
+            elif math.pi * 0.5 < normalisedAngle <= math.pi:
+                x = labelx
+                y = labely
+            elif math.pi < normalisedAngle <= math.pi * 1.5:
+                x = labelx - labelWidth
+                y = labely
+            else:
+                x = labelx - labelWidth
+                y = labely - labelHeight
+
+            # draw label with text tick[1]
+            cx.move_to(x, y)
+            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.startAngle - math.pi/2,
+               self.endAngle - math.pi/2)
+        cx.line_to(centerx, centery)
+        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
diff --git a/src/gui/widgets/pycha/.svn/text-base/scatter.py.svn-base b/src/gui/widgets/pycha/.svn/text-base/scatter.py.svn-base
new file mode 100644
index 0000000..a4be761
--- /dev/null
+++ b/src/gui/widgets/pycha/.svn/text-base/scatter.py.svn-base
@@ -0,0 +1,54 @@
+# Copyright(c) 2007-2009 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.line import LineChart
+
+
+class ScatterplotChart(LineChart):
+
+    def _renderChart(self, cx):
+        """Renders a scatterplot"""
+
+        def drawSymbol(point, size=2):
+            ox = point.x * self.area.w + self.area.x
+            oy = point.y * self.area.h + self.area.y
+            cx.move_to(ox-size, oy)
+            cx.line_to(ox+size, oy)
+            cx.move_to(ox, oy-size)
+            cx.line_to(ox, oy+size)
+
+        def preparePath(storeName, size=2):
+            cx.new_path()
+            for point in self.points:
+                if point.name == storeName:
+                    drawSymbol(point, size)
+            cx.close_path()
+
+        cx.save()
+
+        cx.set_line_width(self.options.stroke.width)
+        # TODO: self.options.stroke.shadow
+        for key in self._getDatasetsKeys():
+            cx.set_source_rgb(*self.colorScheme[key])
+            preparePath(key)
+            cx.stroke()
+
+        cx.restore()
+
+    def _renderLines(self, cx):
+        # We don't need lines in the background
+        pass
diff --git a/src/gui/widgets/pycha/.svn/text-base/stackedbar.py.svn-base b/src/gui/widgets/pycha/.svn/text-base/stackedbar.py.svn-base
new file mode 100644
index 0000000..af888c2
--- /dev/null
+++ b/src/gui/widgets/pycha/.svn/text-base/stackedbar.py.svn-base
@@ -0,0 +1,121 @@
+# 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={}):
+        super(StackedBarChart, self).__init__(surface, options)
+        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.area.origin
+                else:
+                    y = 1 - self.area.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.area.origin
+                else:
+                    x = self.area.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/src/gui/widgets/pycha/COPYING b/src/gui/widgets/pycha/COPYING
new file mode 100644
index 0000000..fc8a5de
--- /dev/null
+++ b/src/gui/widgets/pycha/COPYING
@@ -0,0 +1,165 @@
+		   GNU LESSER GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+
+  This version of the GNU Lesser General Public License incorporates
+the terms and conditions of version 3 of the GNU General Public
+License, supplemented by the additional permissions listed below.
+
+  0. Additional Definitions. 
+
+  As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the GNU
+General Public License.
+
+  "The Library" refers to a covered work governed by this License,
+other than an Application or a Combined Work as defined below.
+
+  An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+  A "Combined Work" is a work produced by combining or linking an
+Application with the Library.  The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+  The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+  The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+  1. Exception to Section 3 of the GNU GPL.
+
+  You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+  2. Conveying Modified Versions.
+
+  If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+   a) under this License, provided that you make a good faith effort to
+   ensure that, in the event an Application does not supply the
+   function or data, the facility still operates, and performs
+   whatever part of its purpose remains meaningful, or
+
+   b) under the GNU GPL, with none of the additional permissions of
+   this License applicable to that copy.
+
+  3. Object Code Incorporating Material from Library Header Files.
+
+  The object code form of an Application may incorporate material from
+a header file that is part of the Library.  You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+   a) Give prominent notice with each copy of the object code that the
+   Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the object code with a copy of the GNU GPL and this license
+   document.
+
+  4. Combined Works.
+
+  You may convey a Combined Work under terms of your choice that,
+taken together, effectively do not restrict modification of the
+portions of the Library contained in the Combined Work and reverse
+engineering for debugging such modifications, if you also do each of
+the following:
+
+   a) Give prominent notice with each copy of the Combined Work that
+   the Library is used in it and that the Library and its use are
+   covered by this License.
+
+   b) Accompany the Combined Work with a copy of the GNU GPL and this license
+   document.
+
+   c) For a Combined Work that displays copyright notices during
+   execution, include the copyright notice for the Library among
+   these notices, as well as a reference directing the user to the
+   copies of the GNU GPL and this license document.
+
+   d) Do one of the following:
+
+       0) Convey the Minimal Corresponding Source under the terms of this
+       License, and the Corresponding Application Code in a form
+       suitable for, and under terms that permit, the user to
+       recombine or relink the Application with a modified version of
+       the Linked Version to produce a modified Combined Work, in the
+       manner specified by section 6 of the GNU GPL for conveying
+       Corresponding Source.
+
+       1) Use a suitable shared library mechanism for linking with the
+       Library.  A suitable mechanism is one that (a) uses at run time
+       a copy of the Library already present on the user's computer
+       system, and (b) will operate properly with a modified version
+       of the Library that is interface-compatible with the Linked
+       Version. 
+
+   e) Provide Installation Information, but only if you would otherwise
+   be required to provide such information under section 6 of the
+   GNU GPL, and only to the extent that such information is
+   necessary to install and execute a modified version of the
+   Combined Work produced by recombining or relinking the
+   Application with a modified version of the Linked Version. (If
+   you use option 4d0, the Installation Information must accompany
+   the Minimal Corresponding Source and Corresponding Application
+   Code. If you use option 4d1, you must provide the Installation
+   Information in the manner specified by section 6 of the GNU GPL
+   for conveying Corresponding Source.)
+
+  5. Combined Libraries.
+
+  You may place library facilities that are a work based on the
+Library side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+   a) Accompany the combined library with a copy of the same work based
+   on the Library, uncombined with any other library facilities,
+   conveyed under the terms of this License.
+
+   b) Give prominent notice with the combined library that part of it
+   is a work based on the Library, and explaining where to find the
+   accompanying uncombined form of the same work.
+
+  6. Revised Versions of the GNU Lesser General Public License.
+
+  The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+  Each version is given a distinguishing version number. If the
+Library as you received it specifies that a certain numbered version
+of the GNU Lesser General Public License "or any later version"
+applies to it, you have the option of following the terms and
+conditions either of that published version or of any later version
+published by the Free Software Foundation. If the Library as you
+received it does not specify a version number of the GNU Lesser
+General Public License, you may choose any version of the GNU Lesser
+General Public License ever published by the Free Software Foundation.
+
+  If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/src/gui/widgets/pycha/README.txt b/src/gui/widgets/pycha/README.txt
new file mode 100644
index 0000000..91ec417
--- /dev/null
+++ b/src/gui/widgets/pycha/README.txt
@@ -0,0 +1,160 @@
+.. contents::
+
+=====
+PyCha
+=====
+
+Pycha is a very simple Python package for drawing charts using the great
+`Cairo <http://www.cairographics.org/>`_ library. Its goals are:
+
+ * Lightweight
+
+ * Simple to use
+ 
+ * Nice looking with default values
+ 
+ * Customization 
+
+It won't try to draw any possible chart on earth but draw the most common ones
+nicely. There are some other options you may want to look at like
+`pyCairoChart <http://bettercom.de/de/pycairochart>`_.
+
+Pycha is based on `Plotr <http://solutoire.com/plotr/>`_ which is based on
+`PlotKit <http://www.liquidx.net/plotkit/>`_. Both libraries are written in
+JavaScript and are great for client web programming. I needed the same for the
+server side so that's the reason I ported Plotr to Python. Now we can deliver
+charts to people with JavaScript disabled or embed them in PDF reports.
+
+Pycha is distributed under the terms of the `GNU Lesser General Public License
+<http://www.gnu.org/licenses/lgpl.html>`_.
+
+Documentation
+=============
+
+Installation
+------------
+
+Pycha needs PyCairo to works since it uses the Cairo graphics library. If you
+use Linux you will probably already have it installed so you don't have to do
+anything. If you use Windows these are the recommended steps for installing
+PyCairo:
+
+   1. Grab the latest PyCairo Windows installer from
+      http://ftp.gnome.org/pub/GNOME/binaries/win32/pycairo/ You need to use the
+      one that matches your Python version so take the one ending in -py2.4.exe
+      for Python 2.4 or the one ending in -py2.5.exe for Python 2.5
+   2. Install it in your Python environment (just follow the installation
+      program instructions)
+   3. Put the Cairo dlls inside the pycairo directory inside your site-packages
+      directory or anywhere in your path. You can find the dlls at
+      http://www.gimp.org/%7Etml/gimp/win32/downloads.html Go there and download
+      the following packages:
+
+         1. cairo.zip. You just need the libcairo-2.dll file inside that zip
+         2. libpng.zip. You just need the libpng13.dll file inside that zip
+         3. zlib.zip. You just need the zlib1.dll file inside that zip 
+
+Pycha is distributed as a Python Egg so is quite easy to install. You just need
+to type the following command:
+
+easy_install pycha
+
+And Easy Install will go to the Cheeseshop and grab the last pycha for you. If
+will also install it for you at no extra cost :-)
+
+Tutorial
+--------
+
+Using pycha is quite simple. You always follow the same 5 simple steps:
+
+   1. Create a Cairo surface to draw the chart on
+   2. Build a list of data sets from which your chart will be created
+   3. Customize the chart options.
+   4. Create the chart, add the datasets and render it
+   5. Save the results into a file or do whatever you want with the Cairo
+      surface 
+
+To create the Cairo surface you just need to say the type of surface and its
+dimensions::
+
+   import cairo
+   width, height = (500, 400)
+   surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
+
+Then you should create your data set querying a database or any other data
+source::
+
+   dataSet = (
+     ('dataSet 1', ((0, 1), (1, 3), (2, 2.5))),
+     ('dataSet 2', ((0, 2), (1, 4), (2, 3))),
+     ('dataSet 3', ((0, 5), (1, 1), (2, 0.5))),     
+   )
+
+As you can see, each data set is a tuple where the first element is the name of
+the data set and the second is another tuple composed by points. Each point is a
+two-elements tuple, the first one is the x value and the second the y value.
+
+Not every chart uses all the information of a data set. For example, the Pie
+chart only uses the first point of each dataset and it only uses the y value of
+the point.
+
+Now you may want to specify some options so the chart can be customize changing
+its defaults values. To see the defaults you can check the
+pycha.chart.Chart.__init__ method in the source code. You can use regular
+dictionaries to define your options. For example, imagine you want to hide the
+legend and use a different color for the background::
+
+   options = {
+       'legend': {'hide': True},
+       'background': {'color': '#f0f0f0'},
+   }
+
+Now we are ready to instantiate the chart, add the data set and render it::
+
+   import pycha.bar
+   chart = pycha.bar.VerticalBarChart(surface, options)
+   chart.addDataset(dataSet)
+   chart.render()
+
+
+Right now you can choose among 4 different kind of charts:
+
+    * Pie Charts (pycha.pie.PieChart)
+    * Vertical Bar Charts (pycha.bar.VerticalBarChart)
+    * Horizontal Bar Charts (pycha.bar.HorizontalBarChart)
+    * Line Charts (pycha.bar.LineChart)
+    * Scatterplot Charts (pycha.scatter.ScatterplotChart)
+
+Finally you can write the surface to a graphic file or anything you want using
+the cairo library::
+
+   surface.write_to_png('output.png')
+
+That's it! You can see more examples in the examples directory of the source
+code.
+
+Documentation
+-------------
+
+Adam Przywecki has done a fantastic work writing documentation for Pycha.
+Check it out at http://pycha.yourwei.com/
+
+
+Development
+-----------
+
+You can get the last bleeding edge version of pycha by getting a checkout of
+the subversion repository::
+
+   svn co http://www.lorenzogil.com/svn/pycha/trunk pycha
+
+Don't forget to check the 
+`Release Notes <http://www.lorenzogil.com/projects/pycha/wiki/ReleaseNotes/>`_ 
+for each version to learn the new features and incompatible changes. 
+
+Contact
+-------
+
+There is a mailing list about PyCha at http://groups.google.com/group/pycha 
+You can join it to ask questions about its use or simply to talk about its
+development. Your ideas and feedback are greatly appreciated! 
diff --git a/src/gui/widgets/pycha/__init__.py b/src/gui/widgets/pycha/__init__.py
new file mode 100644
index 0000000..3aabacf
--- /dev/null
+++ b/src/gui/widgets/pycha/__init__.py
@@ -0,0 +1,18 @@
+# Copyright(c) 2007-2009 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.5.1dev"
diff --git a/src/gui/widgets/pycha/bar.py b/src/gui/widgets/pycha/bar.py
new file mode 100644
index 0000000..f009fb6
--- /dev/null
+++ b/src/gui/widgets/pycha/bar.py
@@ -0,0 +1,243 @@
+# Copyright(c) 2007-2009 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 chart import Chart, uniqueIndices
+from color import hex2rgb
+
+
+class BarChart(Chart):
+
+    def __init__(self, surface=None, options={}):
+        super(BarChart, self).__init__(surface, options)
+        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.area.x + self.area.w * bar.x
+            y = self.area.y + self.area.h * bar.y
+            w = self.area.w * bar.w
+            h = self.area.h * bar.h
+
+            if w < 1 or h < 1:
+                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()
+
+            # 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))
+
+                label = unicode(bar.yval)
+                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:
+                xval, yval = item
+                x = (((xval - self.minxval) * self.xscale)
+                    + self.barMargin + (i * self.barWidthForSet))
+                w = self.barWidthForSet
+                h = abs(yval) * self.yscale
+                if yval > 0:
+                    y = (1.0 - h) - self.area.origin
+                else:
+                    y = 1 - self.area.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.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)
+
+
+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:
+                xval, yval = item
+                y = (((xval - self.minxval) * self.xscale)
+                     + self.barMargin + (i * self.barWidthForSet))
+                h = self.barWidthForSet
+                w = abs(yval) * self.yscale
+                if yval > 0:
+                    x = self.area.origin
+                else:
+                    x = self.area.origin - 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)
+
+    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"""
+        ticks = self.xticks
+        for tick in ticks:
+            self._renderLine(cx, tick, True)
+
+    def _getShadowRectangle(self, x, y, w, h):
+        return (x, y-2, w+2, h+4)
+
+    def _renderXAxis(self, cx):
+        """Draws the horizontal line representing the X axis"""
+        cx.new_path()
+        cx.move_to(self.area.x, self.area.y + self.area.h)
+        cx.line_to(self.area.x + self.area.w, self.area.y + self.area.h)
+        cx.close_path()
+        cx.stroke()
+
+    def _renderYAxis(self, cx):
+        # draws the vertical line representing the Y axis
+        cx.new_path()
+        cx.move_to(self.area.x + self.area.origin * self.area.w,
+                   self.area.y)
+        cx.line_to(self.area.x + self.area.origin * self.area.w,
+                   self.area.y + self.area.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.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)
+
+
+class Rect(object):
+
+    def __init__(self, x, y, w, h, xval, yval, name):
+        self.x, self.y, self.w, self.h = x, y, w, h
+        self.xval, self.yval = xval, yval
+        self.name = name
+
+    def __str__(self):
+        return ("<pycha.bar.Rect@(%.2f, %.2f) %.2fx%.2f (%.2f, %.2f) %s>"
+                % (self.x, self.y, self.w, self.h, self.xval, self.yval,
+                   self.name))
diff --git a/src/gui/widgets/pycha/chart.py b/src/gui/widgets/pycha/chart.py
new file mode 100644
index 0000000..56e6b99
--- /dev/null
+++ b/src/gui/widgets/pycha/chart.py
@@ -0,0 +1,718 @@
+# Copyright(c) 2007-2009 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 color import ColorScheme, hex2rgb, DEFAULT_COLOR
+
+
+class Chart(object):
+
+    def __init__(self, surface, options={}):
+        # 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.area = None # chart area without padding or text labels
+        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.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
+
+    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)
+        self._renderBackground(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
+
+        # calculate area data
+        surface_width, surface_height = self.getSurfaceSize()
+        width = (surface_width
+                 - self.options.padding.left - self.options.padding.right)
+        height = (surface_height
+                  - self.options.padding.top - self.options.padding.bottom)
+
+        if self.minyval * self.maxyval < 0: # different signs
+            origin = abs(self.minyval) * self.yscale
+        else:
+            origin = 0
+
+        self.area = Area(self.options.padding.left,
+                         self.options.padding.top,
+                         width, height, origin)
+
+    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)
+            while 0.0 <= pos <= 1.0:
+                self.xticks.append((pos, 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))
+            while 0.0 <= pos <= 1.0:
+                self.yticks.append((pos, 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:
+                    self.yticks.append((pos, round(yval, prec)))
+
+    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.rectangle(self.area.x, self.area.y, self.area.w, self.area.h)
+            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"""
+        ticks = self.yticks
+        for tick in ticks:
+            self._renderLine(cx, tick, False)
+
+    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.area.w + self.area.x
+            y1 = self.area.y
+            y2 = y1 + self.area.h
+        else:
+            x1 = self.area.x
+            x2 = x1 + self.area.w
+            y1 = y2 = tick[0] * self.area.h + self.area.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 _renderYTick(self, cx, tick):
+        """Aux method for _renderAxis"""
+
+        if callable(tick):
+            return
+
+        x = self.area.x
+        y = self.area.y + tick[0] * self.area.h
+
+        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 = unicode(tick[1])
+        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.show_text(label)
+
+        return label
+
+    def _renderXTick(self, cx, tick, fontAscent):
+        if callable(tick):
+            return
+
+        x = self.area.x + tick[0] * self.area.w
+        y = self.area.y + self.area.h
+
+        cx.new_path()
+        cx.move_to(x, y)
+        cx.line_to(x, y + self.options.axis.tickSize)
+        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 = unicode(tick[1])
+        extents = cx.text_extents(label)
+        labelWidth = extents[2]
+        labelHeight = extents[3]
+
+        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:
+            cx.move_to(x - labelWidth / 2.0,
+                       y + self.options.axis.tickSize
+                       + fontAscent + 4.0)
+            cx.show_text(label)
+        return label
+
+    def _getTickSize(self, cx, ticks, rotate):
+        tickExtents = [cx.text_extents(unicode(tick[1]))[2:4]
+                       for tick in ticks]
+        tickWidth = tickHeight = 0.0
+        if tickExtents:
+            tickHeight = self.options.axis.tickSize + 4.0
+            tickWidth = self.options.axis.tickSize + 4.0
+            widths, heights = zip(*tickExtents)
+            maxWidth, maxHeight = max(widths), max(heights)
+            if rotate:
+                radians = math.radians(rotate)
+                sinRadians = math.sin(radians)
+                cosRadians = math.cos(radians)
+                maxHeight = maxWidth * sinRadians + maxHeight * cosRadians
+                maxWidth = maxWidth * cosRadians + maxHeight * sinRadians
+            tickWidth += maxWidth
+            tickHeight += maxHeight
+        return tickWidth, tickHeight
+
+    def _renderAxisLabel(self, cx, tickWidth, tickHeight, label, x, y,
+                         vertical=False):
+        cx.new_path()
+        cx.select_font_face(self.options.axis.labelFont,
+                            cairo.FONT_SLANT_NORMAL,
+                            cairo.FONT_WEIGHT_BOLD)
+        cx.set_font_size(self.options.axis.labelFontSize)
+        labelWidth = cx.text_extents(label)[2]
+        fontAscent = cx.font_extents()[0]
+        if vertical:
+            cx.move_to(x, y + labelWidth / 2)
+            radians = math.radians(90)
+            cx.rotate(-radians)
+        else:
+            cx.move_to(x - labelWidth / 2.0, y + fontAscent)
+
+        cx.show_text(label)
+
+    def _renderYAxis(self, cx):
+        """Draws the vertical line represeting the Y axis"""
+        cx.new_path()
+        cx.move_to(self.area.x, self.area.y)
+        cx.line_to(self.area.x, self.area.y + self.area.h)
+        cx.close_path()
+        cx.stroke()
+
+    def _renderXAxis(self, cx):
+        """Draws the horizontal line representing the X axis"""
+        cx.new_path()
+        cx.move_to(self.area.x,
+                   self.area.y + self.area.h * (1.0 - self.area.origin))
+        cx.line_to(self.area.x + self.area.w,
+                   self.area.y + self.area.h * (1.0 - self.area.origin))
+        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:
+                cx.save()
+                rotate = self.options.axis.y.rotate
+                tickWidth, tickHeight = self._getTickSize(cx, self.yticks,
+                                                          rotate)
+                label = unicode(self.options.axis.y.label)
+                x = self.area.x - tickWidth - 4.0
+                y = self.area.y + 0.5 * self.area.h
+                self._renderAxisLabel(cx, tickWidth, tickHeight, label, x, y,
+                                      True)
+                cx.restore()
+
+            self._renderYAxis(cx)
+
+        if not self.options.axis.x.hide:
+            fontAscent = cx.font_extents()[0]
+            if self.xticks:
+                for tick in self.xticks:
+                    self._renderXTick(cx, tick, fontAscent)
+
+            if self.options.axis.x.label:
+                cx.save()
+                rotate = self.options.axis.x.rotate
+                tickWidth, tickHeight = self._getTickSize(cx, self.xticks,
+                                                          rotate)
+                label = unicode(self.options.axis.x.label)
+                x = self.area.x + self.area.w / 2.0
+                y = self.area.y + self.area.h + tickHeight + 4.0
+                self._renderAxisLabel(cx, tickWidth, tickHeight, label, x, y,
+                                      False)
+                cx.restore()
+
+            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)
+
+            title = unicode(self.options.title)
+            extents = cx.text_extents(title)
+            titleWidth = extents[2]
+
+            x = self.area.x + self.area.w / 2.0 - titleWidth / 2.0
+            y = cx.font_extents()[0] # font ascent
+
+            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()
+        for key in keys:
+            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.stroke.width)
+        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, y, w, h, origin=0.0):
+        self.x, self.y, self.w, self.h = x, y, w, h
+        self.origin = origin
+
+    def __str__(self):
+        msg = "<pycha.chart.Area@(%.2f, %.2f) %.2f x %.2f Origin: %.2f>"
+        return  msg % (self.x, self.y, self.w, self.h, self.origin)
+
+
+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,
+        labelWidth=50.0,
+        tickFont='Tahoma',
+        tickFontSize=9,
+        x=Option(
+            hide=False,
+            ticks=None,
+            tickCount=10,
+            tickPrecision=1,
+            range=None,
+            rotate=None,
+            label=None,
+            interval=0,
+        ),
+        y=Option(
+            hide=False,
+            ticks=None,
+            tickCount=10,
+            tickPrecision=1,
+            range=None,
+            rotate=None,
+            label=None,
+            interval=0,
+        ),
+    ),
+    background=Option(
+        hide=False,
+        baseColor=None,
+        chartColor='#f5f5f5',
+        lineColor='#ffffff',
+        lineWidth=1.5,
+    ),
+    legend=Option(
+        opacity=0.8,
+        borderColor='#000000',
+        hide=False,
+        position=Option(top=20, left=40, bottom=None, right=None),
+    ),
+    padding=Option(
+        left=30,
+        right=30,
+        top=30,
+        bottom=30,
+    ),
+    stroke=Option(
+        color='#ffffff',
+        hide=False,
+        shadow=True,
+        width=2
+    ),
+    yvals=Option(
+        show=False,
+        inside=False,
+        fontSize=11,
+        fontColor='#000000',
+    ),
+    fillOpacity=1.0,
+    shouldFill=True,
+    barWidthFillFraction=0.75,
+    pieRadius=0.4,
+    colorScheme=Option(
+        name='gradient',
+        args=Option(
+            initialColor=DEFAULT_COLOR,
+            colors=None,
+            ),
+    ),
+    title=None,
+    titleFont='Tahoma',
+    titleFontSize=12,
+)
diff --git a/src/gui/widgets/pycha/color.py b/src/gui/widgets/pycha/color.py
new file mode 100644
index 0000000..65c19bd
--- /dev/null
+++ b/src/gui/widgets/pycha/color.py
@@ -0,0 +1,210 @@
+# Copyright(c) 2007-2009 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
+
+DEFAULT_COLOR = '#3c581a'
+
+
+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 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/src/gui/widgets/pycha/line.py b/src/gui/widgets/pycha/line.py
new file mode 100644
index 0000000..2195ee2
--- /dev/null
+++ b/src/gui/widgets/pycha/line.py
@@ -0,0 +1,123 @@
+# Copyright(c) 2007-2009 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={}):
+        super(LineChart, self).__init__(surface, options)
+        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.area.x, self.area.y + self.area.h)
+                offset = (1.0 - self.area.origin) * self.area.h
+                cx.move_to(self.area.x, self.area.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.area.w + self.area.x,
+                                   point.y * self.area.h + self.area.y)
+                        firstPoint = False
+                        continue
+                    cx.line_to(point.x * self.area.w + self.area.x,
+                               point.y * self.area.h + self.area.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.area.origin) * self.area.h + self.area.y
+                cx.line_to(lastX * self.area.w + self.area.x, y)
+                cx.line_to(self.area.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/src/gui/widgets/pycha/pie.py b/src/gui/widgets/pycha/pie.py
new file mode 100644
index 0000000..b0cd381
--- /dev/null
+++ b/src/gui/widgets/pycha/pie.py
@@ -0,0 +1,211 @@
+# Copyright(c) 2007-2009 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
+from pycha.color import hex2rgb
+
+
+class PieChart(Chart):
+
+    def __init__(self, surface=None, options={}):
+        super(PieChart, self).__init__(surface, options)
+        self.slices = []
+        self.centerx = 0
+        self.centery = 0
+        self.radius = 0
+
+    def _updateChart(self):
+        """Evaluates measures for pie charts"""
+        self.centerx = self.area.x + self.area.w * 0.5
+        self.centery = self.area.y + self.area.h * 0.5
+        self.radius = min(self.area.w * self.options.pieRadius,
+                          self.area.h * self.options.pieRadius)
+
+        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
+
+        self.slices = []
+        for slice in slices:
+            angle += fraction
+            if slice['value'][1] > 0:
+                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 _renderBackground(self, cx):
+        """Renders the background 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))
+            x, y, w, h = 0, 0, self.area.w, self.area.h
+            w += self.options.padding.left + self.options.padding.right
+            h += self.options.padding.top + self.options.padding.bottom
+            cx.rectangle(x, y, w, h)
+            cx.fill()
+
+        cx.restore()
+
+    def _renderChart(self, cx):
+        """Renders a pie chart"""
+        cx.set_line_join(cairo.LINE_JOIN_ROUND)
+
+        if self.options.stroke.shadow:
+            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.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.radius)
+                    cx.fill()
+
+                if not self.options.stroke.hide:
+                    slice.draw(cx, self.centerx, self.centery, self.radius)
+                    cx.set_line_width(self.options.stroke.width)
+                    cx.set_source_rgb(*hex2rgb(self.options.stroke.color))
+                    cx.stroke()
+
+        cx.restore()
+
+    def _renderAxis(self, cx):
+        """Renders the axis for pie charts"""
+        if self.options.axis.x.hide or not self.xticks:
+            return
+
+        self.xlabels = []
+        lookup = dict([(slice.xval, slice) for slice in self.slices])
+
+
+        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 tick in self.xticks:
+            slice = lookup[tick[0]]
+
+            normalisedAngle = slice.getNormalisedAngle()
+
+            big_radius = self.radius + 10
+            labelx = self.centerx + math.sin(normalisedAngle) * big_radius
+            labely = self.centery - math.cos(normalisedAngle) * big_radius
+
+            label = tick[1]
+            extents = cx.text_extents(label)
+            labelWidth = extents[2]
+            labelHeight = extents[3]
+            x = y = 0
+
+            if normalisedAngle <= math.pi * 0.5:
+                x = labelx
+                y = labely - labelHeight
+            elif math.pi * 0.5 < normalisedAngle <= math.pi:
+                x = labelx
+                y = labely
+            elif math.pi < normalisedAngle <= math.pi * 1.5:
+                x = labelx - labelWidth
+                y = labely
+            else:
+                x = labelx - labelWidth
+                y = labely - labelHeight
+
+            # draw label with text tick[1]
+            cx.move_to(x, y)
+            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.startAngle - math.pi/2,
+               self.endAngle - math.pi/2)
+        cx.line_to(centerx, centery)
+        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
diff --git a/src/gui/widgets/pycha/scatter.py b/src/gui/widgets/pycha/scatter.py
new file mode 100644
index 0000000..a4be761
--- /dev/null
+++ b/src/gui/widgets/pycha/scatter.py
@@ -0,0 +1,54 @@
+# Copyright(c) 2007-2009 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.line import LineChart
+
+
+class ScatterplotChart(LineChart):
+
+    def _renderChart(self, cx):
+        """Renders a scatterplot"""
+
+        def drawSymbol(point, size=2):
+            ox = point.x * self.area.w + self.area.x
+            oy = point.y * self.area.h + self.area.y
+            cx.move_to(ox-size, oy)
+            cx.line_to(ox+size, oy)
+            cx.move_to(ox, oy-size)
+            cx.line_to(ox, oy+size)
+
+        def preparePath(storeName, size=2):
+            cx.new_path()
+            for point in self.points:
+                if point.name == storeName:
+                    drawSymbol(point, size)
+            cx.close_path()
+
+        cx.save()
+
+        cx.set_line_width(self.options.stroke.width)
+        # TODO: self.options.stroke.shadow
+        for key in self._getDatasetsKeys():
+            cx.set_source_rgb(*self.colorScheme[key])
+            preparePath(key)
+            cx.stroke()
+
+        cx.restore()
+
+    def _renderLines(self, cx):
+        # We don't need lines in the background
+        pass
diff --git a/src/gui/widgets/pycha/stackedbar.py b/src/gui/widgets/pycha/stackedbar.py
new file mode 100644
index 0000000..af888c2
--- /dev/null
+++ b/src/gui/widgets/pycha/stackedbar.py
@@ -0,0 +1,121 @@
+# 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={}):
+        super(StackedBarChart, self).__init__(surface, options)
+        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.area.origin
+                else:
+                    y = 1 - self.area.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.area.origin
+                else:
+                    x = self.area.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)



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]