From eb963cda8183b4a59c259f0d4288e4946ee39687 Mon Sep 17 00:00:00 2001 From: jaraco Date: Fri, 4 Apr 2008 22:09:15 +0000 Subject: [PATCH] Completed first syntax-error free Pie.py Bumped to 1.1 Added setup.cfg --- lib/SVG/Pie.py | 394 ++++++++++++++++++++++++++++++++++++++++++++ lib/SVG/__init__.py | 2 +- setup.cfg | 3 + setup.py | 2 +- 4 files changed, 399 insertions(+), 2 deletions(-) create mode 100644 lib/SVG/Pie.py create mode 100644 setup.cfg diff --git a/lib/SVG/Pie.py b/lib/SVG/Pie.py new file mode 100644 index 0000000..df28705 --- /dev/null +++ b/lib/SVG/Pie.py @@ -0,0 +1,394 @@ +#!python + +# $Id$ + +import math +import SVG + +def robust_add(a,b): + "Add numbers a and b, treating None as 0" + if a is None: a = 0 + if b is None: b = 0 + return a+b + +class Pie(SVG.Graph): + # === Create presentation quality SVG pie graphs easily + # + # == Synopsis + # + # require 'SVG/Graph/Pie' + # + # fields = %w(Jan Feb Mar) + # data_sales_02 = [12, 45, 21] + # + # graph = SVG::Graph::Pie.new({ + # :height => 500, + # :width => 300, + # :fields => fields, + # }) + # + # graph.add_data({ + # :data => data_sales_02, + # :title => 'Sales 2002', + # }) + # + # print "Content-type: image/svg+xml\r\n\r\n" + # print graph.burn(); + # + # == Description + # + # This object aims to allow you to easily create high quality + # SVG pie graphs. You can either use the default style sheet + # or supply your own. Either way there are many options which can + # be configured to give you control over how the graph is + # generated - with or without a key, display percent on pie chart, + # title, subtitle etc. + # + # = Examples + # + # http://www.germane-software/repositories/public/SVG/test/single.rb + # + # == See also + # + # * SVG::Graph::Graph + # * SVG::Graph::BarHorizontal + # * SVG::Graph::Bar + # * SVG::Graph::Line + # * SVG::Graph::Plot + # * SVG::Graph::TimeSeries + # + # == Author + # + # Sean E. Russell + # + # Copyright 2004 Sean E. Russell + # This software is available under the Ruby license[LICENSE.txt] + # + + "if true, displays a drop shadow for the chart" + show_shadow = True + "Sets the offset of the shadow from the pie chart" + shadow_offset = 10 + + show_data_labels = False + "If true, display the actual field values in the data labels" + show_actual_values = False + "If true, display the percentage value of each pie wedge in the data labels" + show_percent = True + + "If true, display the labels in the key" + show_key_data_labels = True + "If true, display the actual value of the field in the key" + show_key_actual_values = True + "If true, display the percentage value of the wedges in the key" + show_key_percent = False + + "If true, explode the pie (put space between the wedges)" + expanded = False + "If true, expand the largest pie wedge" + expand_greatest = False + "The amount of space between expanded wedges" + expand_gap = 10 + + show_x_labels = False + show_y_labels = False + + "The font size of the data point labels" + datapoint_font_size = 12 + + def add_data(self, data_descriptor): + """ + Add a data set to the graph + + >>> graph.add_data({data:[1,2,3,4]}) + + Note that a 'title' key is ignored. + + Multiple calls to add_data will sum the elements, and the pie will + display the aggregated data. e.g. + + >>> graph.add_data({data:[1,2,3,4]}) + >>> graph.add_data({data:[2,3,5,7]}) + + is the same as: + + graph.add_data({data:[3,5,8,11]}) + """ + self.data = map(robust_add, self.data, data_descriptor['data']) + + def _add_defs(self, defs): + "Add svg definitions" + gradient = self._create_element( + 'filter', + dict( + id='dropshadow', + width=1.2, + height=1.2, + ) + ) + defs.appendChild(gradient) + blur = self._create_element( + 'feGaussianBlur', + dict( + stdDeviation=4, + result='blur', + ) + ) + gradient.appendChild(blur) + + def draw_graph(self): + "Here we don't need the graph (consider refactoring)" + pass + + def get_y_labels(self): + "Definitely consider refactoring" + return [''] + + def get_x_labels(self): + "Okay. I'll refactor after this" + [''] + + def keys(self): + total = reduce(operator.add, self.data) + percent_scale = 100.0 / total + def key(field, value): + result = ' [%s]' % value + if self.show_key_percent: + percent = str(round((v/total*100))) + '%' + result = ' '.join(result, percent) + return result + return map(key, zip(self.fields, self.data)) + + RADIANS = math.pi/180 + + def draw_data(self): + self.graph = self._create_element('g') + self.root.appendChild(self.graph) + background = self._create_element('g') + self.graph.appendChild(background) + midground = self._create_element('g') + self.graph.appendChild(midground) + + is_expanded = (self.expanded or self.expand_greatest) + diameter = min(self.graph_width, self.graph_height) + # the following assumes int(True)==1 and int(False)==0 + diameter -= self.expand_gap * int(is_expanded) + diameter -= self.datapoint_font_size * int(self.show_data_labels) + diameter -= 10 * int(self.show_shadow) + radius = diameter / 2.0 + + xoff = (self.width - diameter) / 2 + yoff = (self.height - self.border_bottom - diameter) + yoff -= 10 * int(self.show_shadow) + transform = 'translate( %(xoff)s %(yoff)s )' % vars() + self.graph.setAttribute('transform', transform) + + wedge_text_pad = 5 + wedge_text_pad = 20 * int(self.show_percent) * int(self.show_data_labels) + + total = reduce(add, self.data) + max_value = max(self.data) + + percent_scale = 100.0 / total + + prev_percent = 0 + rad_mult = 3.6 * RADIANS + for index, (field, value) in enumerate(zip(self.fields, self.data)): + percent = percent_scale * value + + radians = prev_percent * rad_mult + x_start = radius+(math.sin(radians) * radius) + y_start = radius-(math.cos(radians) * radius) + radians = (prev_percent+percent) * rad_mult + x_end = radius+(math.sin(radians) * radius) + y_end = radius-(math.cos(radians) * radius) + percent_greater_fifty = int(percent>=50) + path = ' '.join( + "M%(radius)s,%(radius)s", + "L%(x_start)s,%(y_start)s", + "A%(radius)s,%(radius)s", + "0,", + "%(percent_greater_fifty)s,1,", + "%(x_end)s %(y_end)s Z") + path = path % vars() + + wedge = self._create_element( + 'path', + dict({ + 'd': path, + 'class': 'fill%s' % (index+1), + }) + ) + foreground.appendChild(wedge) + + translate = None + tx = 0 + ty = 0 + half_percent = prev_percent + percent / 2 + radians = half_percent * rad_mult + + if self.show_shadow: + shadow = self._create_element( + 'path', + dict( + d=path, + filter='url(%s)' % self.dropshadow, + style='fill: #ccc; stroke: none', + ) + ) + background.appendChild(shadow) + clear = self._create_element( + 'path', + dict( + d=path, + # note, this probably only works when the background + # is also #fff + style="fill:#fff; stroke:none;", + ) + ) + medground.appendChild(clear) + + if self.expanded or (expand_greatest and value == max_value): + tx = (math.sin(radians) * self.expand_gap) + ty = -(math.cos(radians) * self.expand_gap) + translate = "translate( %(tx)s %(ty)s )" % vars() + wedge.setAttribute('transform', translate) + clear.setAtrtibute('transform', translate) + + if self.show_shadow: + shadow_tx = self.shadow_offset + tx + shadow_ty = self.shadow_offset + ty + translate = 'translate( %(shadow_tx)s %(shadow_ty)s )' + shadow.setAttribute('transform', translate) + + if self.show_data_labels and value != 0: + label = [] + if self.show_key_data_labels: + label.append(field) + if self.show_actual_values: + label.append('[%s]' % value) + if self.show_percent: + label.append(round(percent)+'%') + label = ' '.join(label) + + msr = math.sin(radians) + mcr = math.cos(radians) + tx = radius + (msr * radius) + ty = radius -(mcr * radius) + + if self.expanded or (self.expand_greatest and value == max_value): + tx += (msr * self.expand_gap) + ty -= (mcr * self.expand_gap) + + label = self._create_element( + 'text', + dict({ + 'x':str(tx), + 'y':str(ty), + 'class':'dataPointLabel', + 'style':'stroke: #fff; stroke-width: 2;', + }) + ) + label.appendChild(self._doc.createTextNode(label)) + foreground.appendChild(label) + + label = self._create_element( + 'text', + dict({ + 'x':str(tx), + 'y':str(ty), + 'class': 'dataPointLabel', + }) + ) + label.appendChild(self._doc.createTextNode(label)) + foreground.appendChild(label) + + prev_percent += percent + + def round(self, val, to): + return round(val,to) + + def get_css(self): + return """\ +.dataPointLabel{ + fill: #000000; + text-anchor:middle; + font-size: #{datapoint_font_size}px; + font-family: "Arial", sans-serif; + font-weight: normal; +} + +/* key - MUST match fill styles */ +.key1,.fill1{ + fill: #ff0000; + fill-opacity: 0.7; + stroke: none; + stroke-width: 1px; +} +.key2,.fill2{ + fill: #0000ff; + fill-opacity: 0.7; + stroke: none; + stroke-width: 1px; +} +.key3,.fill3{ + fill-opacity: 0.7; + fill: #00ff00; + stroke: none; + stroke-width: 1px; +} +.key4,.fill4{ + fill-opacity: 0.7; + fill: #ffcc00; + stroke: none; + stroke-width: 1px; +} +.key5,.fill5{ + fill-opacity: 0.7; + fill: #00ccff; + stroke: none; + stroke-width: 1px; +} +.key6,.fill6{ + fill-opacity: 0.7; + fill: #ff00ff; + stroke: none; + stroke-width: 1px; +} +.key7,.fill7{ + fill-opacity: 0.7; + fill: #00ff99; + stroke: none; + stroke-width: 1px; +} +.key8,.fill8{ + fill-opacity: 0.7; + fill: #ffff00; + stroke: none; + stroke-width: 1px; +} +.key9,.fill9{ + fill-opacity: 0.7; + fill: #cc6666; + stroke: none; + stroke-width: 1px; +} +.key10,.fill10{ + fill-opacity: 0.7; + fill: #663399; + stroke: none; + stroke-width: 1px; +} +.key11,.fill11{ + fill-opacity: 0.7; + fill: #339900; + stroke: none; + stroke-width: 1px; +} +.key12,.fill12{ + fill-opacity: 0.7; + fill: #9966FF; + stroke: none; + stroke-width: 1px; +} +""" diff --git a/lib/SVG/__init__.py b/lib/SVG/__init__.py index 9a617e6..e39b432 100644 --- a/lib/SVG/__init__.py +++ b/lib/SVG/__init__.py @@ -1,7 +1,7 @@ #!python # -*- coding: UTF-8 -*- -__all__ = ('Plot', 'TimeSeries', 'Bar') +__all__ = ('Plot', 'TimeSeries', 'Bar', 'Pie') from xml.dom import minidom as dom from operator import itemgetter diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..fa84e53 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,3 @@ +[egg_info] +tag_build = dev +tag_svn_revision = true \ No newline at end of file diff --git a/setup.py b/setup.py index 22d9145..8f5fa0c 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ use_setuptools() from setuptools import setup, find_packages setup(name = "svg-chart", - version = "1.0.2", + version = "1.1", description = "Python SVG Charting Library", author = "Jason R. Coombs", author_email = "jaraco@jaraco.com",