mirror of https://github.com/Kozea/pygal.git
jaraco
17 years ago
4 changed files with 399 additions and 2 deletions
@ -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 <serATgermaneHYPHENsoftwareDOTcom> |
||||
# |
||||
# 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; |
||||
} |
||||
""" |
Loading…
Reference in new issue