Python to generate nice looking SVG graph http://pygal.org/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

293 lines
9.7 KiB

import math
import itertools
from lxml import etree
from svg.charts.graph import Graph
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
RADIANS = math.pi/180
class Pie(Graph):
"""
A presentation-quality SVG pie graph
Synopsis
========
from svg.charts.pie import Pie
fields = ['Jan', 'Feb', 'Mar']
data_sales_02 = [12, 45, 21]
graph = Pie(dict(
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.
"""
"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
stylesheet_names = Graph.stylesheet_names + ['pie.css']
def add_data(self, data_descriptor):
"""
Add a data set to the graph
>>> graph.add_data({data:[1,2,3,4]}) # doctest: +SKIP
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]}) # doctest: +SKIP
>>> graph.add_data({data:[2,3,5,7]}) # doctest: +SKIP
is the same as:
>>> graph.add_data({data:[3,5,8,11]}) # doctest: +SKIP
If data is added of with differing lengths, the corresponding
values will be assumed to be zero.
>>> graph.add_data({data:[1,2,3,4]}) # doctest: +SKIP
>>> graph.add_data({data:[5,7]}) # doctest: +SKIP
is the same as:
>>> graph.add_data({data:[5,7]}) # doctest: +SKIP
>>> graph.add_data({data:[1,2,3,4]}) # doctest: +SKIP
and
>>> graph.add_data({data:[6,9,3,4]}) # doctest: +SKIP
"""
pairs = itertools.izip_longest(self.data, data_descriptor['data'])
self.data = list(itertools.starmap(robust_add, pairs))
def add_defs(self, defs):
"Add svg definitions"
etree.SubElement(
defs,
'filter',
id='dropshadow',
width='1.2',
height='1.2',
)
etree.SubElement(
defs,
'feGaussianBlur',
stdDeviation='4',
result='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"
return ['']
def keys(self):
total = sum(self.data)
percent_scale = 100.0 / total
def key(field, value):
result = [field]
result.append('[%s]' % value)
if self.show_key_percent:
percent = str(round((v/total*100))) + '%'
result.append(percent)
return ' '.join(result)
return map(key, self.fields, self.data)
def draw_data(self):
self.graph = etree.SubElement(self.root, 'g')
background = etree.SubElement(self.graph, 'g')
# midground is somewhere between the background and the foreground
midground = etree.SubElement(self.graph, 'g')
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.set('transform', transform)
wedge_text_pad = 5
wedge_text_pad = 20 * int(self.show_percent) * int(self.show_data_labels)
total = sum(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 = etree.SubElement(
self.foreground,
'path',
{
'd': path,
'class': 'fill%s' % (index+1),
}
)
translate = None
tx = 0
ty = 0
half_percent = prev_percent + percent / 2
radians = half_percent * rad_mult
if self.show_shadow:
shadow = etree.SubElement(
background,
'path',
d=path,
filter='url(#dropshadow)',
style='fill: #ccc; stroke: none',
)
clear = etree.SubElement(
midground,
'path',
d=path,
# note, this probably only works when the background
# is also #fff
# consider getting the style from the stylesheet
style="fill:#fff; stroke:none;",
)
if self.expanded or (self.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.set('transform', translate)
clear.set('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)' % vars()
shadow.set('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('%d%%' % 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_node = etree.SubElement(
self.foreground,
'text',
{
'x':str(tx),
'y':str(ty),
'class':'dataPointLabel',
'style':'stroke: #fff; stroke-width: 2;',
}
)
label_node.text = label
label_node = etree.SubElement(
self.foreground,
'text',
{
'x':str(tx),
'y':str(ty),
'class': 'dataPointLabel',
}
)
label_node.text = label
prev_percent += percent
def round(self, val, to):
return round(val,to)