Browse Source

Library now uses lxml for XML processing. This technique has a much cleaner and robust implementation. I tried using the Python 2.5 element tree, but it doesn't support processing instructions (as far as I can tell).

pull/8/head
jaraco 16 years ago
parent
commit
e95aead46c
  1. 2
      setup.py
  2. 9
      src/svg/charts/bar.py
  3. 256
      src/svg/charts/graph.py
  4. 34
      src/svg/charts/line.py
  5. 98
      src/svg/charts/pie.py
  6. 23
      src/svg/charts/plot.py
  7. 6
      src/svg/charts/schedule.py

2
setup.py

@ -17,7 +17,7 @@ setup(name = "svg.charts",
install_requires=[ install_requires=[
'python-dateutil>=1.4', 'python-dateutil>=1.4',
'cssutils>=0.9.5.1', 'cssutils>=0.9.5.1',
# TODO: consider lxml 'lxml>=2.0',
], ],
license = "MIT", license = "MIT",
long_description = """\ long_description = """\

9
src/svg/charts/bar.py

@ -1,6 +1,7 @@
#!python #!python
from svg.charts.graph import Graph
from itertools import chain from itertools import chain
from lxml import etree
from svg.charts.graph import Graph
__all__ = ('VerticalBar', 'HorizontalBar') __all__ = ('VerticalBar', 'HorizontalBar')
@ -178,14 +179,13 @@ class VerticalBar(Bar):
if self.stack == 'side': if self.stack == 'side':
left += bar_width * dataset_count left += bar_width * dataset_count
rect = self._create_element('rect', { rect = etree.SubElement(self.graph, 'rect', {
'x': str(left), 'x': str(left),
'y': str(top), 'y': str(top),
'width': str(bar_width), 'width': str(bar_width),
'height': str(length), 'height': str(length),
'class': 'fill%s' % (dataset_count+1), 'class': 'fill%s' % (dataset_count+1),
}) })
self.graph.appendChild(rect)
self.make_datapoint_text(left + bar_width/2.0, top-6, value) self.make_datapoint_text(left + bar_width/2.0, top-6, value)
@ -236,14 +236,13 @@ class HorizontalBar(Bar):
# left is 0 if value is negative # left is 0 if value is negative
left = (abs(min_value) + min(value, 0)) * unit_size left = (abs(min_value) + min(value, 0)) * unit_size
rect = self._create_element('rect', { rect = etree.SubElement(self.graph, 'rect', {
'x': str(left), 'x': str(left),
'y': str(top), 'y': str(top),
'width': str(length), 'width': str(length),
'height': str(bar_height), 'height': str(bar_height),
'class': 'fill%s' % (dataset_count+1), 'class': 'fill%s' % (dataset_count+1),
}) })
self.graph.appendChild(rect)
self.make_datapoint_text(left+length+5, top+y_mod, value, self.make_datapoint_text(left+length+5, top+y_mod, value,
"text-anchor: start; ") "text-anchor: start; ")

256
src/svg/charts/graph.py

@ -1,17 +1,17 @@
#!python #!python
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
from xml.dom import minidom as dom
from operator import itemgetter from operator import itemgetter
from itertools import islice from itertools import islice
import cssutils import cssutils
import pkg_resources import pkg_resources
from lxml import etree
try: try:
import zlib import zlib
__have_zlib = True
except ImportError: except ImportError:
__have_zlib = False zlib = None
def sort_multiple(arrays): def sort_multiple(arrays):
"sort multiple lists (of equal size) using the first list for the sort keys" "sort multiple lists (of equal size) using the first list for the sort keys"
@ -97,10 +97,12 @@ class Graph(object):
y_title_font_size= 14 y_title_font_size= 14
key_font_size= 10 key_font_size= 10
css_inline= False css_inline= False
add_popups= False add_popups= False
top_align = top_font = right_align = right_font = 0 top_align = top_font = right_align = right_font = 0
compress = False
def __init__(self, config = {}): def __init__(self, config = {}):
"""Initialize the graph object with the graph settings.""" """Initialize the graph object with the graph settings."""
@ -147,35 +149,40 @@ class Graph(object):
self.data = [] self.data = []
def burn(self): def burn(self):
"""This method processes the template with the data and """
This method processes the template with the data and
config which has been set and returns the resulting SVG. config which has been set and returns the resulting SVG.
This method will croak unless at least one data set has This method will croak unless at least one data set has
been added to the graph object. been added to the graph object.
Ex: graph.burn()""" Ex: graph.burn()
"""
if not self.data: raise ValueError("No data available") if not self.data: raise ValueError("No data available")
if hasattr(self, 'calculations'): self.calculations() if hasattr(self, 'calculations'): self.calculations()
self.start_svg() self.start_svg()
self.calculate_graph_dimensions() self.calculate_graph_dimensions()
self.foreground = self._create_element("g") self.foreground = etree.Element("g")
self.draw_graph() self.draw_graph()
self.draw_titles() self.draw_titles()
self.draw_legend() self.draw_legend()
self.draw_data() self.draw_data()
self.graph.appendChild(self.foreground) self.graph.append(self.foreground)
self.render_inline_styles() self.render_inline_styles()
data = self._doc.toprettyxml() return self._burn_compressed()
def _burn_compressed(self):
if self.compress and not zlib:
self.root.addprevious(etree.Comment('Python zlib not available for SVGZ'))
data = etree.tostring(self.root, pretty_print=True, xml_declaration=True, encoding='utf-8')
if hasattr(self, 'compress') and self.compress: if self.compress and zlib:
if __have_zlib: data = zlib.compress(data)
data = zlib.compress(data)
else:
data += '<!-- Python zlib not available for SVGZ -->'
return data return data
KEY_BOX_SIZE = 12 KEY_BOX_SIZE = 12
@ -225,30 +232,29 @@ class Graph(object):
"Adds pop-up point information to a graph." "Adds pop-up point information to a graph."
txt_width = len(label) * self.font_size * 0.6 + 10 txt_width = len(label) * self.font_size * 0.6 + 10
tx = x + [5,-5][int(x+txt_width > self.width)] tx = x + [5,-5][int(x+txt_width > self.width)]
t = self._create_element('text')
anchor = ['start', 'end'][x+txt_width > self.width] anchor = ['start', 'end'][x+txt_width > self.width]
style = 'fill: #000; text-anchor: %s;' % anchor style = 'fill: #000; text-anchor: %s;' % anchor
id = 'label-%s' % label id = 'label-%s' % label
attributes = {'x': str(tx), t = etree.SubElement(self.foreground, 'text', {
'y': str(y - self.font_size), 'x': str(tx),
'visibility': 'hidden', 'y': str(y - self.font_size),
'style': style, 'visibility': 'hidden',
'text': label, 'style': style,
'id': id 'text': label,
} 'id': id
map(lambda a: t.setAttribute(*a), attributes.items()) })
self.foreground.appendChild(t)
# Note, prior to the etree conversion, this circle element was never
# added to anything (now it's added to the foreground)
visibility = "document.getElementById(%s).setAttribute('visibility', %%s)" % id visibility = "document.getElementById(%s).setAttribute('visibility', %%s)" % id
t = self._create_element('circle') t = etree.SubElement(self.foreground, 'circle', {
attributes = {'cx': str(x), 'cx': str(x),
'cy': str(y), 'cy': str(y),
'r': 10, 'r': str(10),
'style': 'opacity: 0;', 'style': 'opacity: 0;',
'onmouseover': visibility % 'visible', 'onmouseover': visibility % 'visible',
'onmouseout': visibility % 'hidden', 'onmouseout': visibility % 'hidden',
} })
map(lambda a: t.setAttribute(*a), attributes.items())
def calculate_bottom_margin(self): def calculate_bottom_margin(self):
"""Override this (and call super) to change the margin to the bottom """Override this (and call super) to change the margin to the bottom
@ -270,28 +276,27 @@ class Graph(object):
def draw_graph(self): def draw_graph(self):
transform = 'translate (%s %s)' % (self.border_left, self.border_top) transform = 'translate (%s %s)' % (self.border_left, self.border_top)
self.graph = self._create_element('g', {'transform': transform}) self.graph = etree.SubElement(self.root, 'g', transform=transform)
self.root.appendChild(self.graph)
self.graph.appendChild(self._create_element('rect', { etree.SubElement(self.graph, 'rect', {
'x': '0', 'x': '0',
'y': '0', 'y': '0',
'width': str(self.graph_width), 'width': str(self.graph_width),
'height': str(self.graph_height), 'height': str(self.graph_height),
'class': 'graphBackground' 'class': 'graphBackground'
})) })
#Axis #Axis
self.graph.appendChild(self._create_element('path', { etree.SubElement(self.graph, 'path', {
'd': 'M 0 0 v%s' % self.graph_height, 'd': 'M 0 0 v%s' % self.graph_height,
'class': 'axis', 'class': 'axis',
'id': 'xAxis' 'id': 'xAxis'
})) })
self.graph.appendChild(self._create_element('path', { etree.SubElement(self.graph, 'path', {
'd': 'M 0 %s h%s' % (self.graph_height, self.graph_width), 'd': 'M 0 %s h%s' % (self.graph_height, self.graph_width),
'class': 'axis', 'class': 'axis',
'id': 'yAxis' 'id': 'yAxis'
})) })
self.draw_x_labels() self.draw_x_labels()
self.draw_y_labels() self.draw_y_labels()
@ -303,21 +308,22 @@ class Graph(object):
def make_datapoint_text(self, x, y, value, style=''): def make_datapoint_text(self, x, y, value, style=''):
if self.show_data_values: if self.show_data_values:
e = self._create_element('text', { # first lay down the text in a wide white stroke to
# differentiate it from the background
e = etree.SubElement(self.foreground, 'text', {
'x': str(x), 'x': str(x),
'y': str(y), 'y': str(y),
'class': 'dataPointLabel', 'class': 'dataPointLabel',
'style': '%(style)s stroke: #fff; stroke-width: 2;' % vars(), 'style': '%(style)s stroke: #fff; stroke-width: 2;' % vars(),
}) })
e.appendChild(self._doc.createTextNode(str(value))) e.text = str(value)
self.foreground.appendChild(e) # then lay down the text in the specified style
e = self._create_element('text', { e = etree.SubElement(self.foreground, 'text', {
'x': str(x), 'x': str(x),
'y': str(y), 'y': str(y),
'class': 'dataPointLabel'}) 'class': 'dataPointLabel'})
e.appendChild(self._doc.createTextNode(str(value))) e.text = str(value)
if style: e.setAttribute('style', style) if style: e.set('style', style)
self.foreground.appendChild(e)
def draw_x_labels(self): def draw_x_labels(self):
"Draw the X axis labels" "Draw the X axis labels"
@ -334,9 +340,8 @@ class Graph(object):
def draw_x_label(self, label): def draw_x_label(self, label):
label_width = self.field_width() label_width = self.field_width()
index, label = label index, label = label
text = self._create_element('text', {'class': 'xAxisLabels'}) text = etree.SubElement(self.graph, 'text', {'class': 'xAxisLabels'})
text.appendChild(self._doc.createTextNode(label)) text.text = label
self.graph.appendChild(text)
x = index * label_width + self.x_label_offset(label_width) x = index * label_width + self.x_label_offset(label_width)
y = self.graph_height + self.x_label_font_size + 3 y = self.graph_height + self.x_label_font_size + 3
@ -346,22 +351,21 @@ class Graph(object):
stagger = self.x_label_font_size + 5 stagger = self.x_label_font_size + 5
y += stagger y += stagger
graph_height = self.graph_height graph_height = self.graph_height
path = self._create_element('path', { path = etree.SubElement(self.graph, 'path', {
'd': 'M%(x)f %(graph_height)f v%(stagger)d' % vars(), 'd': 'M%(x)f %(graph_height)f v%(stagger)d' % vars(),
'class': 'staggerGuideLine' 'class': 'staggerGuideLine'
}) })
self.graph.appendChild(path)
text.setAttribute('x', str(x)) text.set('x', str(x))
text.setAttribute('y', str(y)) text.set('y', str(y))
if self.rotate_x_labels: if self.rotate_x_labels:
transform = 'rotate(90 %d %d) translate(0 -%d)' % \ transform = 'rotate(90 %d %d) translate(0 -%d)' % \
(x, y-self.x_label_font_size, self.x_label_font_size/4) (x, y-self.x_label_font_size, self.x_label_font_size/4)
text.setAttribute('transform', transform) text.set('transform', transform)
text.setAttribute('style', 'text-anchor: start') text.set('style', 'text-anchor: start')
else: else:
text.setAttribute('style', 'text-anchor: middle') text.set('style', 'text-anchor: middle')
def y_label_offset(self, height): def y_label_offset(self, height):
"""Where in the Y area the label is drawn """Where in the Y area the label is drawn
@ -400,9 +404,8 @@ class Graph(object):
def draw_y_label(self, label): def draw_y_label(self, label):
label_height = self.field_height() label_height = self.field_height()
index, label = label index, label = label
text = self._create_element('text', {'class': 'yAxisLabels'}) text = etree.SubElement(self.graph, 'text', {'class': 'yAxisLabels'})
text.appendChild(self._doc.createTextNode(label)) text.text = label
self.graph.appendChild(text)
y = self.y_offset - (label_height * index) y = self.y_offset - (label_height * index)
x = {True: 0, False:-3}[self.rotate_y_labels] x = {True: 0, False:-3}[self.rotate_y_labels]
@ -410,23 +413,22 @@ class Graph(object):
if self.stagger_y_labels and (index % 2): if self.stagger_y_labels and (index % 2):
stagger = self.y_label_font_size + 5 stagger = self.y_label_font_size + 5
x -= stagger x -= stagger
path = self._create_element('path', { path = etree.SubElement(self.graph, 'path', {
'd': 'M%(x)f %(y)f h%(stagger)d' % vars(), 'd': 'M%(x)f %(y)f h%(stagger)d' % vars(),
'class': 'staggerGuideLine' 'class': 'staggerGuideLine'
}) })
self.graph.appendChild(path)
text.setAttribute('x', str(x)) text.set('x', str(x))
text.setAttribute('y', str(y)) text.set('y', str(y))
if self.rotate_y_labels: if self.rotate_y_labels:
transform = 'translate(-%d 0) rotate (90 %d %d)' % \ transform = 'translate(-%d 0) rotate (90 %d %d)' % \
(self.font_size, x, y) (self.font_size, x, y)
text.setAttribute('transform', transform) text.set('transform', transform)
text.setAttribute('style', 'text-anchor: middle') text.set('style', 'text-anchor: middle')
else: else:
text.setAttribute('y', str(y - self.y_label_font_size/2)) text.set('y', str(y - self.y_label_font_size/2))
text.setAttribute('style', 'text-anchor: end') text.set('style', 'text-anchor: end')
def draw_x_guidelines(self, label_height, count): def draw_x_guidelines(self, label_height, count):
"Draw the X-axis guidelines" "Draw the X-axis guidelines"
@ -435,11 +437,9 @@ class Graph(object):
for count in range(1,count): for count in range(1,count):
start = label_height*count start = label_height*count
stop = self.graph_height stop = self.graph_height
path = self._create_element('path', { path = etree.SubElement(self.graph, 'path', {
'd': 'M %(start)s 0 v%(stop)s' % vars(), 'd': 'M %(start)s 0 v%(stop)s' % vars(),
'class': 'guideLines'}) 'class': 'guideLines'})
self.graph.appendChild(path)
def draw_y_guidelines(self, label_height, count): def draw_y_guidelines(self, label_height, count):
"Draw the Y-axis guidelines" "Draw the Y-axis guidelines"
@ -447,10 +447,9 @@ class Graph(object):
for count in range(1, count): for count in range(1, count):
start = self.graph_height - label_height*count start = self.graph_height - label_height*count
stop = self.graph_width stop = self.graph_width
path = self._create_element('path', { path = etree.SubElement(self.graph, 'path', {
'd': 'M 0 %(start)s h%(stop)s' % vars(), 'd': 'M 0 %(start)s h%(stop)s' % vars(),
'class': 'guideLines'}) 'class': 'guideLines'})
self.graph.appendChild(path)
def draw_titles(self): def draw_titles(self):
"Draws the graph title and subtitle" "Draws the graph title and subtitle"
@ -460,23 +459,21 @@ class Graph(object):
if self.show_y_title: self.draw_y_title() if self.show_y_title: self.draw_y_title()
def draw_graph_title(self): def draw_graph_title(self):
text = self._create_element('text', { text = etree.SubElement(self.root, 'text', {
'x': str(self.width / 2), 'x': str(self.width / 2),
'y': str(self.title_font_size), 'y': str(self.title_font_size),
'class': 'mainTitle'}) 'class': 'mainTitle'})
text.appendChild(self._doc.createTextNode(self.graph_title)) text.text = self.graph_title
self.root.appendChild(text)
def draw_graph_subtitle(self): def draw_graph_subtitle(self):
y_subtitle_options = [subtitle_font_size, title_font_size+10] y_subtitle_options = [subtitle_font_size, title_font_size+10]
y_subtitle = y_subtitle_options[self.show_graph_title] y_subtitle = y_subtitle_options[self.show_graph_title]
text = self._create_element('text', { text = etree.SubElement(self.root, 'text', {
'x': str(self.width/2), 'x': str(self.width/2),
'y': str(y_subtitle), 'y': str(y_subtitle),
'class': 'subTitle', 'class': 'subTitle',
}) })
text.appendChild(self._doc.createTextNode(self.graph_title)) text.text = self.graph_title
self.root.appendChild(text)
def draw_x_title(self): def draw_x_title(self):
y = self.graph_height + self.border_top + self.x_title_font_size y = self.graph_height + self.border_top + self.x_title_font_size
@ -486,13 +483,12 @@ class Graph(object):
y += y_size y += y_size
x = self.width / 2 x = self.width / 2
text = self._create_element('text', { text = etree.SubElement(self.root, 'text', {
'x': str(x), 'x': str(x),
'y': str(y), 'y': str(y),
'class': 'xAxisTitle', 'class': 'xAxisTitle',
}) })
text.appendChild(self._doc.createTextNode(self.x_title)) text.text = self.x_title
self.root.appendChild(text)
def draw_y_title(self): def draw_y_title(self):
x = self.y_title_font_size x = self.y_title_font_size
@ -503,46 +499,42 @@ class Graph(object):
x -= 3 x -= 3
rotate = 90 rotate = 90
y = self.height / 2 y = self.height / 2
text = self._create_element('text', { text = etree.SubElement(self.root, 'text', {
'x': str(x), 'x': str(x),
'y': str(y), 'y': str(y),
'class': 'yAxisTitle', 'class': 'yAxisTitle',
}) })
text.appendChild(self._doc.createTextNode(self.y_title)) text.text = self.y_title
text.setAttribute('transform', 'rotate(%(rotate)d, %(x)s, %(y)s)' % vars()) text.set('transform', 'rotate(%(rotate)d, %(x)s, %(y)s)' % vars())
self.root.appendChild(text)
def keys(self): def keys(self):
return map(itemgetter('title'), self.data) return map(itemgetter('title'), self.data)
def draw_legend(self): def draw_legend(self):
if self.key: if self.key:
group = self._create_element('g') group = etree.SubElement(self.root, 'g')
self.root.appendChild(group)
for key_count, key_name in enumerate(self.keys()): for key_count, key_name in enumerate(self.keys()):
y_offset = (self.KEY_BOX_SIZE * key_count) + (key_count * 5) y_offset = (self.KEY_BOX_SIZE * key_count) + (key_count * 5)
rect = self._create_element('rect', { etree.SubElement(group, 'rect', {
'x': '0', 'x': '0',
'y': str(y_offset), 'y': str(y_offset),
'width': str(self.KEY_BOX_SIZE), 'width': str(self.KEY_BOX_SIZE),
'height': str(self.KEY_BOX_SIZE), 'height': str(self.KEY_BOX_SIZE),
'class': 'key%s' % (key_count + 1), 'class': 'key%s' % (key_count + 1),
}) })
group.appendChild(rect) text = etree.SubElement(group, 'text', {
text = self._create_element('text', {
'x': str(self.KEY_BOX_SIZE + 5), 'x': str(self.KEY_BOX_SIZE + 5),
'y': str(y_offset + self.KEY_BOX_SIZE), 'y': str(y_offset + self.KEY_BOX_SIZE),
'class': 'keyText'}) 'class': 'keyText'})
text.appendChild(self._doc.createTextNode(key_name)) text.text = key_name
group.appendChild(text)
if self.key_position == 'right': if self.key_position == 'right':
x_offset = self.graph_width + self.border_left + 10 x_offset = self.graph_width + self.border_left + 10
y_offset = self.border_top + 20 y_offset = self.border_top + 20
if self.key_position == 'bottom': if self.key_position == 'bottom':
x_offset, y_offset = self.calculate_offsets_bottom() x_offset, y_offset = self.calculate_offsets_bottom()
group.setAttribute('transform', 'translate(%(x_offset)d %(y_offset)d)' % vars()) group.set('transform', 'translate(%(x_offset)d %(y_offset)d)' % vars())
def calculate_offsets_bottom(self): def calculate_offsets_bottom(self):
x_offset = self.border_left + 20 x_offset = self.border_left + 20
@ -587,46 +579,52 @@ class Graph(object):
def start_svg(self): def start_svg(self):
"Base SVG Document Creation" "Base SVG Document Creation"
impl = dom.getDOMImplementation() SVG_NAMESPACE = 'http://www.w3.org/2000/svg'
self._doc = impl.createDocument(None, 'svg', None) SVG = '{%s}' % SVG_NAMESPACE
self.root = self._doc.documentElement NSMAP = {
if hasattr(self, 'style_sheet_href'): None: SVG_NAMESPACE,
pi = self._doc.createProcessingInstruction('xml-stylesheet', 'xlink': 'http://www.w3.org/1999/xlink',
'href="%s" type="text/css"' % self.style_sheet_href) 'a3': 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/',
attributes = { }
self.root = etree.Element(SVG+"svg", attrib={
'width': str(self.width), 'width': str(self.width),
'height': str(self.height), 'height': str(self.height),
'viewBox': '0 0 %s %s' % (self.width, self.height), 'viewBox': '0 0 %s %s' % (self.width, self.height),
'xmlns': 'http://www.w3.org/2000/svg', '{http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/}scriptImplementation': 'Adobe',
'xmlns:xlink': 'http://www.w3.org/1999/xlink', }, nsmap=NSMAP)
'xmlns:a3': 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/', if hasattr(self, 'style_sheet_href'):
'a3:scriptImplementation': 'Adobe'} pi = etree.ProcessingInstruction(
map(lambda a: self.root.setAttribute(*a), attributes.items()) 'xml-stylesheet',
self.root.appendChild(self._doc.createComment(' Created with SVG.Graph ')) 'href="%s" type="text/css"' % self.style_sheet_href
self.root.appendChild(self._doc.createComment(' SVG.Graph by Jason R. Coombs ')) )
self.root.appendChild(self._doc.createComment(' Based on SVG::Graph by Sean E. Russel ')) self.root.addprevious(pi)
self.root.appendChild(self._doc.createComment(' Based on Perl SVG:TT:Graph by Leo Lapworth & Stephan Morgan '))
self.root.appendChild(self._doc.createComment(' '+'/'*66)) comment_strings = (
' Created with SVG.Graph ',
defs = self._create_element('defs') ' SVG.Graph by Jason R. Coombs ',
' Based on SVG::Graph by Sean E. Russel ',
' Based on Perl SVG:TT:Graph by Leo Lapworth & Stephan Morgan ',
' '+'/'*66,
)
map(self.root.append, map(etree.Comment, comment_strings))
defs = etree.SubElement(self.root, 'defs')
self.add_defs(defs) self.add_defs(defs)
self.root.appendChild(defs)
if not hasattr(self, 'style_sheet_href') and not self.css_inline: if not hasattr(self, 'style_sheet_href') and not self.css_inline:
self.root.appendChild(self._doc.createComment(' include default stylesheet if none specified ')) self.root.append(etree.Comment(' include default stylesheet if none specified '))
style = self._create_element('style', {'type': 'text/css'}) style = etree.SubElement(defs, 'style', type='text/css')
defs.appendChild(style) # TODO: the text was previously escaped in a CDATA declaration... how
style_data = self._doc.createCDATASection(self.get_stylesheet().cssText) # to do that with etree?
style.appendChild(style_data) style.text = self.get_stylesheet().cssText
self.root.appendChild(self._doc.createComment('SVG Background')) self.root.append(etree.Comment('SVG Background'))
rect = self._create_element('rect', { rect = etree.SubElement(self.root, 'rect', {
'width': str(self.width), 'width': str(self.width),
'height': str(self.height), 'height': str(self.height),
'x': '0', 'x': '0',
'y': '0', 'y': '0',
'class': 'svgBackground'}) 'class': 'svgBackground'})
self.root.appendChild(rect)
def calculate_graph_dimensions(self): def calculate_graph_dimensions(self):
self.calculate_left_margin() self.calculate_left_margin()
@ -661,12 +659,6 @@ class Graph(object):
def css_file(self): def css_file(self):
return self.__class__.__name__.lower() + '.css' return self.__class__.__name__.lower() + '.css'
def _create_element(self, nodeName, attributes={}):
"Create an XML node and set the attributes from a dict"
node = self._doc.createElement(nodeName)
map(lambda a: node.setAttribute(*a), attributes.items())
return node
class class_dict(object): class class_dict(object):
"Emulates a dictionary, but retrieves class attributes" "Emulates a dictionary, but retrieves class attributes"
def __init__(self, obj): def __init__(self, obj):

34
src/svg/charts/line.py

@ -3,10 +3,10 @@
# $Id$ # $Id$
from operator import itemgetter, add from operator import itemgetter, add
from util import flatten from lxml import etree
from util import flatten, float_range
from svg.charts.graph import Graph from svg.charts.graph import Graph
from util import float_range
class Line(Graph): class Line(Graph):
""" === Create presentation quality SVG line graphs easily """ === Create presentation quality SVG line graphs easily
@ -193,7 +193,6 @@ class Line(Graph):
area_path = "V#@graph_height" area_path = "V#@graph_height"
origin = coord_format(get_coords(0,0)) origin = coord_format(get_coords(0,0))
p = self._create_element('path')
d = ' '.join(( d = ' '.join((
'M', 'M',
origin, origin,
@ -202,29 +201,28 @@ class Line(Graph):
area_path, area_path,
'Z' 'Z'
)) ))
p.setAttribute('d', d) etree.SubElement(self.graph, 'path', {
p.setAttribute('class', 'fill%(line_n)s' % vars()) 'class': 'fill%(line_n)s' % vars(),
self.graph.appendChild(p) 'd': d,
})
# now draw the line itself # now draw the line itself
p = self._create_element('path') etree.SubElement(self.graph, 'path', {
p.setAttribute('d', 'M0 '+self.graph_height+' L'+line_path) 'd': 'M0 '+self.graph_height+' L'+line_path,
p.setAttribute('class', 'line%(line_n)s' % vars()) 'class': 'line%(line_n)s' % vars(),
self.graph.appendChild(p) })
if self.show_data_points or self.show_data_values: if self.show_data_points or self.show_data_values:
for i, value in enumerate(cum_sum): for i, value in enumerate(cum_sum):
if self.show_data_points: if self.show_data_points:
circle = self._create_element( circle = etree.SubElement(
self.graph,
'circle', 'circle',
dict( {'class': 'dataPoint%(line_n)s' % vars()},
cx = str(field_width*i), cx = str(field_width*i),
cy = str(self.graph_height - value*field_height), cy = str(self.graph_height - value*field_height),
r = '2.5', r = '2.5',
)
) )
circle.setAttribute('class', 'dataPoint%(line_n)s' % vars())
self.graph.appendChild(circle)
self.make_datapoint_text( self.make_datapoint_text(
field_width*i, field_width*i,
self.graph_height - value*field_height - 6, self.graph_height - value*field_height - 6,

98
src/svg/charts/pie.py

@ -4,6 +4,7 @@
import math import math
from operator import add from operator import add
from lxml import etree
from svg.charts.graph import Graph from svg.charts.graph import Graph
def robust_add(a,b): def robust_add(a,b):
@ -121,23 +122,19 @@ class Pie(Graph):
def add_defs(self, defs): def add_defs(self, defs):
"Add svg definitions" "Add svg definitions"
gradient = self._create_element( etree.SubElement(
defs,
'filter', 'filter',
dict( id='dropshadow',
id='dropshadow', width='1.2',
width='1.2', height='1.2',
height='1.2',
)
) )
defs.appendChild(gradient) etree.SubElement(
blur = self._create_element( defs,
'feGaussianBlur', 'feGaussianBlur',
dict( stdDeviation='4',
stdDeviation='4', result='blur',
result='blur',
)
) )
gradient.appendChild(blur)
def draw_graph(self): def draw_graph(self):
"Here we don't need the graph (consider refactoring)" "Here we don't need the graph (consider refactoring)"
@ -149,7 +146,7 @@ class Pie(Graph):
def get_x_labels(self): def get_x_labels(self):
"Okay. I'll refactor after this" "Okay. I'll refactor after this"
[''] return ['']
def keys(self): def keys(self):
total = reduce(add, self.data) total = reduce(add, self.data)
@ -164,12 +161,10 @@ class Pie(Graph):
return map(key, self.fields, self.data) return map(key, self.fields, self.data)
def draw_data(self): def draw_data(self):
self.graph = self._create_element('g') self.graph = etree.SubElement(self.root, 'g')
self.root.appendChild(self.graph) background = etree.SubElement(self.graph, 'g')
background = self._create_element('g') # midground is somewhere between the background and the foreground
self.graph.appendChild(background) midground = etree.SubElement(self.graph, 'g')
midground = self._create_element('g')
self.graph.appendChild(midground)
is_expanded = (self.expanded or self.expand_greatest) is_expanded = (self.expanded or self.expand_greatest)
diameter = min(self.graph_width, self.graph_height) diameter = min(self.graph_width, self.graph_height)
@ -183,7 +178,7 @@ class Pie(Graph):
yoff = (self.height - self.border_bottom - diameter) yoff = (self.height - self.border_bottom - diameter)
yoff -= 10 * int(self.show_shadow) yoff -= 10 * int(self.show_shadow)
transform = 'translate(%(xoff)s %(yoff)s)' % vars() transform = 'translate(%(xoff)s %(yoff)s)' % vars()
self.graph.setAttribute('transform', transform) self.graph.set('transform', transform)
wedge_text_pad = 5 wedge_text_pad = 5
wedge_text_pad = 20 * int(self.show_percent) * int(self.show_data_labels) wedge_text_pad = 20 * int(self.show_percent) * int(self.show_data_labels)
@ -214,14 +209,14 @@ class Pie(Graph):
"%(x_end)s %(y_end)s Z")) "%(x_end)s %(y_end)s Z"))
path = path % vars() path = path % vars()
wedge = self._create_element( wedge = etree.SubElement(
self.foreground,
'path', 'path',
dict({ {
'd': path, 'd': path,
'class': 'fill%s' % (index+1), 'class': 'fill%s' % (index+1),
}) }
) )
self.foreground.appendChild(wedge)
translate = None translate = None
tx = 0 tx = 0
@ -230,38 +225,35 @@ class Pie(Graph):
radians = half_percent * rad_mult radians = half_percent * rad_mult
if self.show_shadow: if self.show_shadow:
shadow = self._create_element( shadow = etree.SubElement(
background,
'path', 'path',
dict( d=path,
d=path, filter='url(#dropshadow)',
filter='url(#dropshadow)', style='fill: #ccc; stroke: none',
style='fill: #ccc; stroke: none',
)
) )
background.appendChild(shadow) clear = etree.SubElement(
clear = self._create_element( midground,
'path', 'path',
dict( d=path,
d=path, # note, this probably only works when the background
# note, this probably only works when the background # is also #fff
# is also #fff # consider getting the style from the stylesheet
style="fill:#fff; stroke:none;", style="fill:#fff; stroke:none;",
)
) )
midground.appendChild(clear)
if self.expanded or (self.expand_greatest and value == max_value): if self.expanded or (self.expand_greatest and value == max_value):
tx = (math.sin(radians) * self.expand_gap) tx = (math.sin(radians) * self.expand_gap)
ty = -(math.cos(radians) * self.expand_gap) ty = -(math.cos(radians) * self.expand_gap)
translate = "translate(%(tx)s %(ty)s)" % vars() translate = "translate(%(tx)s %(ty)s)" % vars()
wedge.setAttribute('transform', translate) wedge.set('transform', translate)
clear.setAttribute('transform', translate) clear.set('transform', translate)
if self.show_shadow: if self.show_shadow:
shadow_tx = self.shadow_offset + tx shadow_tx = self.shadow_offset + tx
shadow_ty = self.shadow_offset + ty shadow_ty = self.shadow_offset + ty
translate = 'translate(%(shadow_tx)s %(shadow_ty)s)' % vars() translate = 'translate(%(shadow_tx)s %(shadow_ty)s)' % vars()
shadow.setAttribute('transform', translate) shadow.set('transform', translate)
if self.show_data_labels and value != 0: if self.show_data_labels and value != 0:
label = [] label = []
@ -282,28 +274,28 @@ class Pie(Graph):
tx += (msr * self.expand_gap) tx += (msr * self.expand_gap)
ty -= (mcr * self.expand_gap) ty -= (mcr * self.expand_gap)
label_node = self._create_element( label_node = etree.SubElement(
self.foreground,
'text', 'text',
dict({ {
'x':str(tx), 'x':str(tx),
'y':str(ty), 'y':str(ty),
'class':'dataPointLabel', 'class':'dataPointLabel',
'style':'stroke: #fff; stroke-width: 2;', 'style':'stroke: #fff; stroke-width: 2;',
}) }
) )
label_node.appendChild(self._doc.createTextNode(label)) label_node.text = label
self.foreground.appendChild(label_node)
label_node = self._create_element( label_node = etree.SubElement(
self.foreground,
'text', 'text',
dict({ {
'x':str(tx), 'x':str(tx),
'y':str(ty), 'y':str(ty),
'class': 'dataPointLabel', 'class': 'dataPointLabel',
}) }
) )
label_node.appendChild(self._doc.createTextNode(label)) label_node.text = label
self.foreground.appendChild(label_node)
prev_percent += percent prev_percent += percent

23
src/svg/charts/plot.py

@ -1,6 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
from svg.charts.graph import Graph
from itertools import izip, count, chain from itertools import izip, count, chain
from lxml import etree
from svg.charts.graph import Graph
from util import float_range from util import float_range
@ -236,15 +238,13 @@ class Plot(Graph):
lpath = self.get_lpath(graph_points) lpath = self.get_lpath(graph_points)
if self.area_fill: if self.area_fill:
graph_height = self.graph_height graph_height = self.graph_height
path = self._create_element('path', { path = etree.SubElement(self.graph, 'path', {
'd': 'M%(x_start)f %(graph_height)f %(lpath)s V%(graph_height)f Z' % vars(), 'd': 'M%(x_start)f %(graph_height)f %(lpath)s V%(graph_height)f Z' % vars(),
'class': 'fill%(line)d' % vars()}) 'class': 'fill%(line)d' % vars()})
self.graph.appendChild(path)
if self.draw_lines_between_points: if self.draw_lines_between_points:
path = self._create_element('path', { path = etree.SubElement(self.graph, 'path', {
'd': 'M%(x_start)f %(y_start)f %(lpath)s' % vars(), 'd': 'M%(x_start)f %(y_start)f %(lpath)s' % vars(),
'class': 'line%(line)d' % vars()}) 'class': 'line%(line)d' % vars()})
self.graph.appendChild(path)
self.draw_data_points(line, data_points, graph_points) self.draw_data_points(line, data_points, graph_points)
self._draw_constant_lines() self._draw_constant_lines()
del self.__transform_parameters del self.__transform_parameters
@ -261,18 +261,16 @@ class Plot(Graph):
"Draw a constant line on the y-axis with the label" "Draw a constant line on the y-axis with the label"
start = self.transform_output_coordinates((0, value))[1] start = self.transform_output_coordinates((0, value))[1]
stop = self.graph_width stop = self.graph_width
path = self._create_element('path', { path = etree.SubElement(self.graph, 'path', {
'd': 'M 0 %(start)s h%(stop)s' % vars(), 'd': 'M 0 %(start)s h%(stop)s' % vars(),
'class': 'constantLine'}) 'class': 'constantLine'})
if style: if style:
path['style'] = style path.set('style', style)
self.graph.appendChild(path) text = etree.SubElement(self.graph, 'text', {
text = self._create_element('text', {
'x': str(2), 'x': str(2),
'y': str(start - 2), 'y': str(start - 2),
'class': 'constantLine'}) 'class': 'constantLine'})
text.appendChild(self._doc.createTextNode(label)) text.text = label
self.graph.appendChild(text)
def load_transform_parameters(self): def load_transform_parameters(self):
"Cache the parameters necessary to transform x & y coordinates" "Cache the parameters necessary to transform x & y coordinates"
@ -308,12 +306,11 @@ class Plot(Graph):
and not self.show_data_values: return and not self.show_data_values: return
for ((dx,dy),(gx,gy)) in izip(data_points, graph_points): for ((dx,dy),(gx,gy)) in izip(data_points, graph_points):
if self.show_data_points: if self.show_data_points:
circle = self._create_element('circle', { etree.SubElement(self.graph, 'circle', {
'cx': str(gx), 'cx': str(gx),
'cy': str(gy), 'cy': str(gy),
'r': '2.5', 'r': '2.5',
'class': 'dataPoint%(line)s' % vars()}) 'class': 'dataPoint%(line)s' % vars()})
self.graph.appendChild(circle)
if self.show_data_values: if self.show_data_values:
self.add_popup(gx, gy, self.format(dx, dy)) self.add_popup(gx, gy, self.format(dx, dy))
self.make_datapoint_text(gx, gy-6, dy) self.make_datapoint_text(gx, gy-6, dy)

6
src/svg/charts/schedule.py

@ -3,6 +3,7 @@ import re
from dateutil.parser import parse from dateutil.parser import parse
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from lxml import etree
from svg.charts.graph import Graph from svg.charts.graph import Graph
from util import grouper, date_range, divide_timedelta_float, TimeScale from util import grouper, date_range, divide_timedelta_float, TimeScale
@ -236,14 +237,13 @@ class Schedule(Graph):
bar_width = scale*(x_end-x_start) bar_width = scale*(x_end-x_start)
bar_start = scale*(x_start-x_min) bar_start = scale*(x_start-x_min)
rect = self._create_element('rect', { etree.SubElement(self.graph, 'rect', {
'x': str(bar_start), 'x': str(bar_start),
'y': str(y), 'y': str(y),
'width': str(bar_width), 'width': str(bar_width),
'height': str(subbar_height), 'height': str(subbar_height),
'class': 'fill%s' % (count+1), # TODO: doublecheck that +1 is correct (that's what's in the Ruby code) 'class': 'fill%s' % (count+1),
}) })
self.graph.appendChild(rect)
def _x_range(self): def _x_range(self):

Loading…
Cancel
Save