From 6c0355d7194c60dcb778bb34a3df5a9bac511f37 Mon Sep 17 00:00:00 2001 From: Agustin Zubiaga Date: Mon, 02 Jul 2012 00:48:34 +0000 Subject: Some charts.py fixes (sugarpycha) --- (limited to 'sugarpycha/polygonal.py') diff --git a/sugarpycha/polygonal.py b/sugarpycha/polygonal.py new file mode 100644 index 0000000..b470c87 --- /dev/null +++ b/sugarpycha/polygonal.py @@ -0,0 +1,372 @@ +# Copyright(c) 2011 by Roberto Garcia Carvajal +# +# 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 . + +import math + +import cairo + +from pycha.chart import Chart +from pycha.line import Point +from pycha.color import hex2rgb +from pycha.utils import safe_unicode + + +class PolygonalChart(Chart): + + def __init__(self, surface=None, options={}): + super(PolygonalChart, self).__init__(surface, options) + self.points = [] + + def _updateChart(self): + """Evaluates measures for polygonal charts""" + self.points = [] + + for i, (name, store) in enumerate(self.datasets): + for item in store: + xval, yval = item + x = (xval - self.minxval) * self.xscale + y = 1.0 - (yval - self.minyval) * self.yscale + point = Point(x, y, xval, yval, name) + + if 0.0 <= point.x <= 1.0 and 0.0 <= point.y <= 1.0: + self.points.append(point) + + def _renderBackground(self, cx): + """Renders the background area of the chart""" + if self.options.background.hide: + return + + cx.save() + + if self.options.background.baseColor: + cx.set_source_rgb(*hex2rgb(self.options.background.baseColor)) + cx.paint() + + if self.options.background.chartColor: + cx.set_source_rgb(*hex2rgb(self.options.background.chartColor)) + cx.set_line_width(10.0) + cx.new_path() + init = None + count = len(self.xticks) + for index, tick in enumerate(self.xticks): + ang = math.pi / 2 - index * 2 * math.pi / count + x = (self.layout.chart.x + self.layout.chart.w / 2 + - math.cos(ang) + * min(self.layout.chart.w / 2, self.layout.chart.h / 2)) + y = (self.layout.chart.y + self.layout.chart.h / 2 + - math.sin(ang) + * min(self.layout.chart.w / 2, self.layout.chart.h / 2)) + if init is None: + cx.move_to(x, y) + init = (x, y) + else: + cx.line_to(x, y) + cx.line_to(init[0], init[1]) + cx.close_path() + cx.fill() + + if self.options.background.lineColor: + cx.set_source_rgb(*hex2rgb(self.options.background.lineColor)) + cx.set_line_width(self.options.axis.lineWidth) + self._renderLines(cx) + + cx.restore() + + def _renderLine(self, cx, tick, horiz): + """Aux function for _renderLines""" + + rad = (self.layout.chart.h / 2) * (1 - tick[0]) + cx.new_path() + init = None + count = len(self.xticks) + for index, tick in enumerate(self.xticks): + ang = math.pi / 2 - index * 2 * math.pi / count + x = (self.layout.chart.x + self.layout.chart.w / 2 + - math.cos(ang) * rad) + y = (self.layout.chart.y + self.layout.chart.h / 2 + - math.sin(ang) * rad) + if init is None: + cx.move_to(x, y) + init = (x, y) + else: + cx.line_to(x, y) + cx.line_to(init[0], init[1]) + cx.close_path() + cx.stroke() + + def _renderXAxis(self, cx): + """Draws the horizontal line representing the X axis""" + + count = len(self.xticks) + + centerx = self.layout.chart.x + self.layout.chart.w / 2 + centery = self.layout.chart.y + self.layout.chart.h / 2 + + for i in range(0, count): + offset1 = i * 2 * math.pi / count + offset = math.pi / 2 - offset1 + + rad = self.layout.chart.h / 2 + (r1, r2) = (0, rad + 5) + + x1 = centerx - math.cos(offset) * r1 + x2 = centerx - math.cos(offset) * r2 + y1 = centery - math.sin(offset) * r1 + y2 = centery - math.sin(offset) * r2 + + cx.new_path() + cx.move_to(x1, y1) + cx.line_to(x2, y2) + cx.close_path() + cx.stroke() + + def _renderYTick(self, cx, tick, center): + """Aux method for _renderAxis""" + + i = tick + tick = self.yticks[i] + + count = len(self.yticks) + + if callable(tick): + return + + x = center[0] + y = center[1] - i * (self.layout.chart.h / 2) / count + + cx.new_path() + cx.move_to(x, y) + cx.line_to(x - self.options.axis.tickSize, y) + cx.close_path() + cx.stroke() + + cx.select_font_face(self.options.axis.tickFont, + cairo.FONT_SLANT_NORMAL, + cairo.FONT_WEIGHT_NORMAL) + cx.set_font_size(self.options.axis.tickFontSize) + + label = safe_unicode(tick[1], self.options.encoding) + extents = cx.text_extents(label) + labelWidth = extents[2] + labelHeight = extents[3] + + if self.options.axis.y.rotate: + radians = math.radians(self.options.axis.y.rotate) + cx.move_to(x - self.options.axis.tickSize + - (labelWidth * math.cos(radians)) + - 4, + y + (labelWidth * math.sin(radians)) + + labelHeight / (2.0 / math.cos(radians))) + cx.rotate(-radians) + cx.show_text(label) + cx.rotate(radians) # this is probably faster than a save/restore + else: + cx.move_to(x - self.options.axis.tickSize - labelWidth - 4, + y + labelHeight / 2.0) + cx.rel_move_to(0.0, -labelHeight / 2.0) + cx.show_text(label) + + return label + + def _renderYAxis(self, cx): + """Draws the vertical line for the Y axis""" + + centerx = self.layout.chart.x + self.layout.chart.w / 2 + centery = self.layout.chart.y + self.layout.chart.h / 2 + + offset = math.pi / 2 + + r1 = self.layout.chart.h / 2 + + x1 = centerx - math.cos(offset) * r1 + y1 = centery - math.sin(offset) * r1 + + cx.new_path() + cx.move_to(centerx, centery) + cx.line_to(x1, y1) + cx.close_path() + cx.stroke() + + def _renderAxis(self, cx): + """Renders axis""" + if self.options.axis.x.hide and self.options.axis.y.hide: + return + + cx.save() + cx.set_source_rgb(*hex2rgb(self.options.axis.lineColor)) + cx.set_line_width(self.options.axis.lineWidth) + + centerx = self.layout.chart.x + self.layout.chart.w / 2 + centery = self.layout.chart.y + self.layout.chart.h / 2 + + if not self.options.axis.y.hide: + if self.yticks: + + count = len(self.yticks) + + for i in range(0, count): + self._renderYTick(cx, i, (centerx, centery)) + + if self.options.axis.y.label: + self._renderYAxisLabel(cx, self.options.axis.y.label) + + self._renderYAxis(cx) + + if not self.options.axis.x.hide: + fontAscent = cx.font_extents()[0] + if self.xticks: + + count = len(self.xticks) + + for i in range(0, count): + self._renderXTick(cx, i, fontAscent, (centerx, centery)) + + if self.options.axis.x.label: + self._renderXAxisLabel(cx, self.options.axis.x.label) + + self._renderXAxis(cx) + + cx.restore() + + def _renderXTick(self, cx, i, fontAscent, center): + tick = self.xticks[i] + if callable(tick): + return + + count = len(self.xticks) + cx.select_font_face(self.options.axis.tickFont, + cairo.FONT_SLANT_NORMAL, + cairo.FONT_WEIGHT_NORMAL) + cx.set_font_size(self.options.axis.tickFontSize) + + label = safe_unicode(tick[1], self.options.encoding) + extents = cx.text_extents(label) + labelWidth = extents[2] + labelHeight = extents[3] + + x, y = center + cx.move_to(x, y) + + if self.options.axis.x.rotate: + radians = math.radians(self.options.axis.x.rotate) + cx.move_to(x - (labelHeight * math.cos(radians)), + y + self.options.axis.tickSize + + (labelHeight * math.cos(radians)) + + 4.0) + cx.rotate(radians) + cx.show_text(label) + cx.rotate(-radians) + else: + offset1 = i * 2 * math.pi / count + offset = math.pi / 2 - offset1 + + rad = self.layout.chart.h / 2 + 10 + + x = center[0] - math.cos(offset) * rad + y = center[1] - math.sin(offset) * rad + + cx.move_to(x, y) + cx.rotate(offset - math.pi / 2) + + if math.sin(offset) < 0.0: + cx.rotate(math.pi) + cx.rel_move_to(0.0, 5.0) + + cx.rel_move_to(-labelWidth / 2.0, 0) + cx.show_text(label) + if math.sin(offset) < 0.0: + cx.rotate(-math.pi) + + cx.rotate(-(offset - math.pi / 2)) + return label + + def _renderChart(self, cx): + """Renders a polygonal chart""" + # draw the polygon. + def preparePath(storeName): + cx.new_path() + firstPoint = True + + count = len(self.points) / len(self.datasets) + centerx = self.layout.chart.x + self.layout.chart.w / 2 + centery = self.layout.chart.y + self.layout.chart.h / 2 + + firstPointCoord = None + + for index, point in enumerate(self.points): + if point.name == storeName: + offset1 = index * 2 * math.pi / count + offset = math.pi / 2 - offset1 + + rad = (self.layout.chart.h / 2) * (1 - point.y) + + x = centerx - math.cos(offset) * rad + y = centery - math.sin(offset) * rad + + if firstPointCoord is None: + firstPointCoord = (x, y) + + if not self.options.shouldFill and firstPoint: + # starts the first point of the line + cx.move_to(x, y) + firstPoint = False + continue + cx.line_to(x, y) + + if not firstPointCoord is None: + cx.line_to(firstPointCoord[0], firstPointCoord[1]) + + if self.options.shouldFill: + # Close the path to the start point + y = ((1.0 - self.origin) + * self.layout.chart.h + self.layout.chart.y) + else: + cx.set_source_rgb(*self.colorScheme[storeName]) + cx.stroke() + + cx.save() + cx.set_line_width(self.options.stroke.width) + if self.options.shouldFill: + + def drawLine(storeName): + if self.options.stroke.shadow: + # draw shadow + cx.save() + cx.set_source_rgba(0, 0, 0, 0.15) + cx.translate(2, -2) + preparePath(storeName) + cx.fill() + cx.restore() + + # fill the line + cx.set_source_rgb(*self.colorScheme[storeName]) + preparePath(storeName) + cx.fill() + + if not self.options.stroke.hide: + # draw stroke + cx.set_source_rgb(*hex2rgb(self.options.stroke.color)) + preparePath(storeName) + cx.stroke() + + # draw the lines + for key in self._getDatasetsKeys(): + drawLine(key) + else: + for key in self._getDatasetsKeys(): + preparePath(key) + cx.restore() -- cgit v0.9.1