diff --git a/setup.py b/setup.py index 11968fc..c6b94db 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ setup(name = "svg.charts", install_requires=[ 'python-dateutil>=1.4', 'cssutils>=0.9.5.1', - # TODO: consider lxml + 'lxml>=2.0', ], license = "MIT", long_description = """\ diff --git a/src/svg/charts/bar.py b/src/svg/charts/bar.py index 83af4bc..26fee37 100644 --- a/src/svg/charts/bar.py +++ b/src/svg/charts/bar.py @@ -1,6 +1,7 @@ #!python -from svg.charts.graph import Graph from itertools import chain +from lxml import etree +from svg.charts.graph import Graph __all__ = ('VerticalBar', 'HorizontalBar') @@ -178,14 +179,13 @@ class VerticalBar(Bar): if self.stack == 'side': left += bar_width * dataset_count - rect = self._create_element('rect', { + rect = etree.SubElement(self.graph, 'rect', { 'x': str(left), 'y': str(top), 'width': str(bar_width), 'height': str(length), 'class': 'fill%s' % (dataset_count+1), }) - self.graph.appendChild(rect) 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 = (abs(min_value) + min(value, 0)) * unit_size - rect = self._create_element('rect', { + rect = etree.SubElement(self.graph, 'rect', { 'x': str(left), 'y': str(top), 'width': str(length), 'height': str(bar_height), 'class': 'fill%s' % (dataset_count+1), }) - self.graph.appendChild(rect) self.make_datapoint_text(left+length+5, top+y_mod, value, "text-anchor: start; ") diff --git a/src/svg/charts/graph.py b/src/svg/charts/graph.py index 3d07c24..2088f81 100644 --- a/src/svg/charts/graph.py +++ b/src/svg/charts/graph.py @@ -1,17 +1,17 @@ #!python # -*- coding: UTF-8 -*- -from xml.dom import minidom as dom from operator import itemgetter from itertools import islice import cssutils import pkg_resources +from lxml import etree + try: import zlib - __have_zlib = True except ImportError: - __have_zlib = False + zlib = None def sort_multiple(arrays): "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 key_font_size= 10 - css_inline= False + css_inline= False add_popups= False top_align = top_font = right_align = right_font = 0 + + compress = False def __init__(self, config = {}): """Initialize the graph object with the graph settings.""" @@ -147,35 +149,40 @@ class Graph(object): self.data = [] 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. This method will croak unless at least one data set has been added to the graph object. - Ex: graph.burn()""" + Ex: graph.burn() + """ if not self.data: raise ValueError("No data available") if hasattr(self, 'calculations'): self.calculations() self.start_svg() self.calculate_graph_dimensions() - self.foreground = self._create_element("g") + self.foreground = etree.Element("g") self.draw_graph() self.draw_titles() self.draw_legend() self.draw_data() - self.graph.appendChild(self.foreground) + self.graph.append(self.foreground) 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 __have_zlib: - data = zlib.compress(data) - else: - data += '' - + if self.compress and zlib: + data = zlib.compress(data) + return data KEY_BOX_SIZE = 12 @@ -225,30 +232,29 @@ class Graph(object): "Adds pop-up point information to a graph." txt_width = len(label) * self.font_size * 0.6 + 10 tx = x + [5,-5][int(x+txt_width > self.width)] - t = self._create_element('text') anchor = ['start', 'end'][x+txt_width > self.width] style = 'fill: #000; text-anchor: %s;' % anchor id = 'label-%s' % label - attributes = {'x': str(tx), - 'y': str(y - self.font_size), - 'visibility': 'hidden', - 'style': style, - 'text': label, - 'id': id - } - map(lambda a: t.setAttribute(*a), attributes.items()) - self.foreground.appendChild(t) - + t = etree.SubElement(self.foreground, 'text', { + 'x': str(tx), + 'y': str(y - self.font_size), + 'visibility': 'hidden', + 'style': style, + 'text': label, + 'id': id + }) + + # 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 - t = self._create_element('circle') - attributes = {'cx': str(x), - 'cy': str(y), - 'r': 10, - 'style': 'opacity: 0;', - 'onmouseover': visibility % 'visible', - 'onmouseout': visibility % 'hidden', - } - map(lambda a: t.setAttribute(*a), attributes.items()) + t = etree.SubElement(self.foreground, 'circle', { + 'cx': str(x), + 'cy': str(y), + 'r': str(10), + 'style': 'opacity: 0;', + 'onmouseover': visibility % 'visible', + 'onmouseout': visibility % 'hidden', + }) def calculate_bottom_margin(self): """Override this (and call super) to change the margin to the bottom @@ -270,28 +276,27 @@ class Graph(object): def draw_graph(self): transform = 'translate (%s %s)' % (self.border_left, self.border_top) - self.graph = self._create_element('g', {'transform': transform}) - self.root.appendChild(self.graph) + self.graph = etree.SubElement(self.root, 'g', transform=transform) - self.graph.appendChild(self._create_element('rect', { + etree.SubElement(self.graph, 'rect', { 'x': '0', 'y': '0', 'width': str(self.graph_width), 'height': str(self.graph_height), 'class': 'graphBackground' - })) + }) #Axis - self.graph.appendChild(self._create_element('path', { + etree.SubElement(self.graph, 'path', { 'd': 'M 0 0 v%s' % self.graph_height, 'class': 'axis', '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), 'class': 'axis', 'id': 'yAxis' - })) + }) self.draw_x_labels() self.draw_y_labels() @@ -303,21 +308,22 @@ class Graph(object): def make_datapoint_text(self, x, y, value, style=''): 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), 'y': str(y), 'class': 'dataPointLabel', 'style': '%(style)s stroke: #fff; stroke-width: 2;' % vars(), }) - e.appendChild(self._doc.createTextNode(str(value))) - self.foreground.appendChild(e) - e = self._create_element('text', { + e.text = str(value) + # then lay down the text in the specified style + e = etree.SubElement(self.foreground, 'text', { 'x': str(x), 'y': str(y), 'class': 'dataPointLabel'}) - e.appendChild(self._doc.createTextNode(str(value))) - if style: e.setAttribute('style', style) - self.foreground.appendChild(e) + e.text = str(value) + if style: e.set('style', style) def draw_x_labels(self): "Draw the X axis labels" @@ -334,9 +340,8 @@ class Graph(object): def draw_x_label(self, label): label_width = self.field_width() index, label = label - text = self._create_element('text', {'class': 'xAxisLabels'}) - text.appendChild(self._doc.createTextNode(label)) - self.graph.appendChild(text) + text = etree.SubElement(self.graph, 'text', {'class': 'xAxisLabels'}) + text.text = label x = index * label_width + self.x_label_offset(label_width) y = self.graph_height + self.x_label_font_size + 3 @@ -346,22 +351,21 @@ class Graph(object): stagger = self.x_label_font_size + 5 y += stagger 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(), 'class': 'staggerGuideLine' }) - self.graph.appendChild(path) - text.setAttribute('x', str(x)) - text.setAttribute('y', str(y)) + text.set('x', str(x)) + text.set('y', str(y)) if self.rotate_x_labels: transform = 'rotate(90 %d %d) translate(0 -%d)' % \ (x, y-self.x_label_font_size, self.x_label_font_size/4) - text.setAttribute('transform', transform) - text.setAttribute('style', 'text-anchor: start') + text.set('transform', transform) + text.set('style', 'text-anchor: start') else: - text.setAttribute('style', 'text-anchor: middle') + text.set('style', 'text-anchor: middle') def y_label_offset(self, height): """Where in the Y area the label is drawn @@ -400,9 +404,8 @@ class Graph(object): def draw_y_label(self, label): label_height = self.field_height() index, label = label - text = self._create_element('text', {'class': 'yAxisLabels'}) - text.appendChild(self._doc.createTextNode(label)) - self.graph.appendChild(text) + text = etree.SubElement(self.graph, 'text', {'class': 'yAxisLabels'}) + text.text = label y = self.y_offset - (label_height * index) x = {True: 0, False:-3}[self.rotate_y_labels] @@ -410,23 +413,22 @@ class Graph(object): if self.stagger_y_labels and (index % 2): stagger = self.y_label_font_size + 5 x -= stagger - path = self._create_element('path', { + path = etree.SubElement(self.graph, 'path', { 'd': 'M%(x)f %(y)f h%(stagger)d' % vars(), 'class': 'staggerGuideLine' }) - self.graph.appendChild(path) - text.setAttribute('x', str(x)) - text.setAttribute('y', str(y)) + text.set('x', str(x)) + text.set('y', str(y)) if self.rotate_y_labels: transform = 'translate(-%d 0) rotate (90 %d %d)' % \ (self.font_size, x, y) - text.setAttribute('transform', transform) - text.setAttribute('style', 'text-anchor: middle') + text.set('transform', transform) + text.set('style', 'text-anchor: middle') else: - text.setAttribute('y', str(y - self.y_label_font_size/2)) - text.setAttribute('style', 'text-anchor: end') + text.set('y', str(y - self.y_label_font_size/2)) + text.set('style', 'text-anchor: end') def draw_x_guidelines(self, label_height, count): "Draw the X-axis guidelines" @@ -435,11 +437,9 @@ class Graph(object): for count in range(1,count): start = label_height*count stop = self.graph_height - path = self._create_element('path', { + path = etree.SubElement(self.graph, 'path', { 'd': 'M %(start)s 0 v%(stop)s' % vars(), 'class': 'guideLines'}) - self.graph.appendChild(path) - def draw_y_guidelines(self, label_height, count): "Draw the Y-axis guidelines" @@ -447,10 +447,9 @@ class Graph(object): for count in range(1, count): start = self.graph_height - label_height*count stop = self.graph_width - path = self._create_element('path', { + path = etree.SubElement(self.graph, 'path', { 'd': 'M 0 %(start)s h%(stop)s' % vars(), 'class': 'guideLines'}) - self.graph.appendChild(path) def draw_titles(self): "Draws the graph title and subtitle" @@ -460,23 +459,21 @@ class Graph(object): if self.show_y_title: self.draw_y_title() def draw_graph_title(self): - text = self._create_element('text', { + text = etree.SubElement(self.root, 'text', { 'x': str(self.width / 2), 'y': str(self.title_font_size), 'class': 'mainTitle'}) - text.appendChild(self._doc.createTextNode(self.graph_title)) - self.root.appendChild(text) + text.text = self.graph_title def draw_graph_subtitle(self): y_subtitle_options = [subtitle_font_size, title_font_size+10] 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), 'y': str(y_subtitle), 'class': 'subTitle', }) - text.appendChild(self._doc.createTextNode(self.graph_title)) - self.root.appendChild(text) + text.text = self.graph_title def draw_x_title(self): y = self.graph_height + self.border_top + self.x_title_font_size @@ -486,13 +483,12 @@ class Graph(object): y += y_size x = self.width / 2 - text = self._create_element('text', { + text = etree.SubElement(self.root, 'text', { 'x': str(x), 'y': str(y), 'class': 'xAxisTitle', }) - text.appendChild(self._doc.createTextNode(self.x_title)) - self.root.appendChild(text) + text.text = self.x_title def draw_y_title(self): x = self.y_title_font_size @@ -503,46 +499,42 @@ class Graph(object): x -= 3 rotate = 90 y = self.height / 2 - text = self._create_element('text', { + text = etree.SubElement(self.root, 'text', { 'x': str(x), 'y': str(y), 'class': 'yAxisTitle', }) - text.appendChild(self._doc.createTextNode(self.y_title)) - text.setAttribute('transform', 'rotate(%(rotate)d, %(x)s, %(y)s)' % vars()) - self.root.appendChild(text) + text.text = self.y_title + text.set('transform', 'rotate(%(rotate)d, %(x)s, %(y)s)' % vars()) def keys(self): return map(itemgetter('title'), self.data) def draw_legend(self): if self.key: - group = self._create_element('g') - self.root.appendChild(group) + group = etree.SubElement(self.root, 'g') for key_count, key_name in enumerate(self.keys()): y_offset = (self.KEY_BOX_SIZE * key_count) + (key_count * 5) - rect = self._create_element('rect', { + etree.SubElement(group, 'rect', { 'x': '0', 'y': str(y_offset), 'width': str(self.KEY_BOX_SIZE), 'height': str(self.KEY_BOX_SIZE), 'class': 'key%s' % (key_count + 1), }) - group.appendChild(rect) - text = self._create_element('text', { + text = etree.SubElement(group, 'text', { 'x': str(self.KEY_BOX_SIZE + 5), 'y': str(y_offset + self.KEY_BOX_SIZE), 'class': 'keyText'}) - text.appendChild(self._doc.createTextNode(key_name)) - group.appendChild(text) + text.text = key_name if self.key_position == 'right': x_offset = self.graph_width + self.border_left + 10 y_offset = self.border_top + 20 if self.key_position == '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): x_offset = self.border_left + 20 @@ -587,46 +579,52 @@ class Graph(object): def start_svg(self): "Base SVG Document Creation" - impl = dom.getDOMImplementation() - self._doc = impl.createDocument(None, 'svg', None) - self.root = self._doc.documentElement - if hasattr(self, 'style_sheet_href'): - pi = self._doc.createProcessingInstruction('xml-stylesheet', - 'href="%s" type="text/css"' % self.style_sheet_href) - attributes = { + SVG_NAMESPACE = 'http://www.w3.org/2000/svg' + SVG = '{%s}' % SVG_NAMESPACE + NSMAP = { + None: SVG_NAMESPACE, + 'xlink': 'http://www.w3.org/1999/xlink', + 'a3': 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/', + } + self.root = etree.Element(SVG+"svg", attrib={ 'width': str(self.width), 'height': str(self.height), 'viewBox': '0 0 %s %s' % (self.width, self.height), - 'xmlns': 'http://www.w3.org/2000/svg', - 'xmlns:xlink': 'http://www.w3.org/1999/xlink', - 'xmlns:a3': 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/', - 'a3:scriptImplementation': 'Adobe'} - map(lambda a: self.root.setAttribute(*a), attributes.items()) - self.root.appendChild(self._doc.createComment(' Created with SVG.Graph ')) - 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.appendChild(self._doc.createComment(' Based on Perl SVG:TT:Graph by Leo Lapworth & Stephan Morgan ')) - self.root.appendChild(self._doc.createComment(' '+'/'*66)) - - defs = self._create_element('defs') + '{http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/}scriptImplementation': 'Adobe', + }, nsmap=NSMAP) + if hasattr(self, 'style_sheet_href'): + pi = etree.ProcessingInstruction( + 'xml-stylesheet', + 'href="%s" type="text/css"' % self.style_sheet_href + ) + self.root.addprevious(pi) + + comment_strings = ( + ' Created with SVG.Graph ', + ' 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.root.appendChild(defs) if not hasattr(self, 'style_sheet_href') and not self.css_inline: - self.root.appendChild(self._doc.createComment(' include default stylesheet if none specified ')) - style = self._create_element('style', {'type': 'text/css'}) - defs.appendChild(style) - style_data = self._doc.createCDATASection(self.get_stylesheet().cssText) - style.appendChild(style_data) - - self.root.appendChild(self._doc.createComment('SVG Background')) - rect = self._create_element('rect', { + self.root.append(etree.Comment(' include default stylesheet if none specified ')) + style = etree.SubElement(defs, 'style', type='text/css') + # TODO: the text was previously escaped in a CDATA declaration... how + # to do that with etree? + style.text = self.get_stylesheet().cssText + + self.root.append(etree.Comment('SVG Background')) + rect = etree.SubElement(self.root, 'rect', { 'width': str(self.width), 'height': str(self.height), 'x': '0', 'y': '0', 'class': 'svgBackground'}) - self.root.appendChild(rect) def calculate_graph_dimensions(self): self.calculate_left_margin() @@ -661,12 +659,6 @@ class Graph(object): def css_file(self): 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): "Emulates a dictionary, but retrieves class attributes" def __init__(self, obj): diff --git a/src/svg/charts/line.py b/src/svg/charts/line.py index dd90939..12c3000 100644 --- a/src/svg/charts/line.py +++ b/src/svg/charts/line.py @@ -3,10 +3,10 @@ # $Id$ 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 util import float_range class Line(Graph): """ === Create presentation quality SVG line graphs easily @@ -193,7 +193,6 @@ class Line(Graph): area_path = "V#@graph_height" origin = coord_format(get_coords(0,0)) - p = self._create_element('path') d = ' '.join(( 'M', origin, @@ -202,29 +201,28 @@ class Line(Graph): area_path, 'Z' )) - p.setAttribute('d', d) - p.setAttribute('class', 'fill%(line_n)s' % vars()) - self.graph.appendChild(p) + etree.SubElement(self.graph, 'path', { + 'class': 'fill%(line_n)s' % vars(), + 'd': d, + }) # now draw the line itself - p = self._create_element('path') - p.setAttribute('d', 'M0 '+self.graph_height+' L'+line_path) - p.setAttribute('class', 'line%(line_n)s' % vars()) - self.graph.appendChild(p) + etree.SubElement(self.graph, 'path', { + 'd': 'M0 '+self.graph_height+' L'+line_path, + 'class': 'line%(line_n)s' % vars(), + }) if self.show_data_points or self.show_data_values: for i, value in enumerate(cum_sum): if self.show_data_points: - circle = self._create_element( + circle = etree.SubElement( + self.graph, 'circle', - dict( - cx = str(field_width*i), - cy = str(self.graph_height - value*field_height), - r = '2.5', - ) + {'class': 'dataPoint%(line_n)s' % vars()}, + cx = str(field_width*i), + cy = str(self.graph_height - value*field_height), + r = '2.5', ) - circle.setAttribute('class', 'dataPoint%(line_n)s' % vars()) - self.graph.appendChild(circle) self.make_datapoint_text( field_width*i, self.graph_height - value*field_height - 6, diff --git a/src/svg/charts/pie.py b/src/svg/charts/pie.py index baf6c17..b1aa0ba 100644 --- a/src/svg/charts/pie.py +++ b/src/svg/charts/pie.py @@ -4,6 +4,7 @@ import math from operator import add +from lxml import etree from svg.charts.graph import Graph def robust_add(a,b): @@ -121,23 +122,19 @@ class Pie(Graph): def add_defs(self, defs): "Add svg definitions" - gradient = self._create_element( + etree.SubElement( + defs, 'filter', - dict( - id='dropshadow', - width='1.2', - height='1.2', - ) + id='dropshadow', + width='1.2', + height='1.2', ) - defs.appendChild(gradient) - blur = self._create_element( + etree.SubElement( + defs, 'feGaussianBlur', - dict( - stdDeviation='4', - result='blur', - ) + stdDeviation='4', + result='blur', ) - gradient.appendChild(blur) def draw_graph(self): "Here we don't need the graph (consider refactoring)" @@ -149,7 +146,7 @@ class Pie(Graph): def get_x_labels(self): "Okay. I'll refactor after this" - [''] + return [''] def keys(self): total = reduce(add, self.data) @@ -164,12 +161,10 @@ class Pie(Graph): return map(key, self.fields, self.data) 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) + 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) @@ -183,7 +178,7 @@ class Pie(Graph): 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) + self.graph.set('transform', transform) wedge_text_pad = 5 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")) path = path % vars() - wedge = self._create_element( + wedge = etree.SubElement( + self.foreground, 'path', - dict({ + { 'd': path, 'class': 'fill%s' % (index+1), - }) + } ) - self.foreground.appendChild(wedge) translate = None tx = 0 @@ -230,38 +225,35 @@ class Pie(Graph): radians = half_percent * rad_mult if self.show_shadow: - shadow = self._create_element( + shadow = etree.SubElement( + background, 'path', - dict( - d=path, - filter='url(#dropshadow)', - style='fill: #ccc; stroke: none', - ) + d=path, + filter='url(#dropshadow)', + style='fill: #ccc; stroke: none', ) - background.appendChild(shadow) - clear = self._create_element( + clear = etree.SubElement( + midground, 'path', - dict( - d=path, - # note, this probably only works when the background - # is also #fff - style="fill:#fff; stroke:none;", - ) + 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;", ) - midground.appendChild(clear) 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.setAttribute('transform', translate) - clear.setAttribute('transform', translate) + 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.setAttribute('transform', translate) + shadow.set('transform', translate) if self.show_data_labels and value != 0: label = [] @@ -282,28 +274,28 @@ class Pie(Graph): tx += (msr * self.expand_gap) ty -= (mcr * self.expand_gap) - label_node = self._create_element( + label_node = etree.SubElement( + self.foreground, 'text', - dict({ + { 'x':str(tx), 'y':str(ty), 'class':'dataPointLabel', 'style':'stroke: #fff; stroke-width: 2;', - }) + } ) - label_node.appendChild(self._doc.createTextNode(label)) - self.foreground.appendChild(label_node) + label_node.text = label - label_node = self._create_element( + label_node = etree.SubElement( + self.foreground, 'text', - dict({ + { 'x':str(tx), 'y':str(ty), 'class': 'dataPointLabel', - }) + } ) - label_node.appendChild(self._doc.createTextNode(label)) - self.foreground.appendChild(label_node) + label_node.text = label prev_percent += percent diff --git a/src/svg/charts/plot.py b/src/svg/charts/plot.py index e6c38e1..c97be1c 100644 --- a/src/svg/charts/plot.py +++ b/src/svg/charts/plot.py @@ -1,6 +1,8 @@ #!/usr/bin/env python -from svg.charts.graph import Graph from itertools import izip, count, chain +from lxml import etree + +from svg.charts.graph import Graph from util import float_range @@ -236,15 +238,13 @@ class Plot(Graph): lpath = self.get_lpath(graph_points) if self.area_fill: 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(), 'class': 'fill%(line)d' % vars()}) - self.graph.appendChild(path) 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(), 'class': 'line%(line)d' % vars()}) - self.graph.appendChild(path) self.draw_data_points(line, data_points, graph_points) self._draw_constant_lines() del self.__transform_parameters @@ -261,18 +261,16 @@ class Plot(Graph): "Draw a constant line on the y-axis with the label" start = self.transform_output_coordinates((0, value))[1] stop = self.graph_width - path = self._create_element('path', { + path = etree.SubElement(self.graph, 'path', { 'd': 'M 0 %(start)s h%(stop)s' % vars(), 'class': 'constantLine'}) if style: - path['style'] = style - self.graph.appendChild(path) - text = self._create_element('text', { + path.set('style', style) + text = etree.SubElement(self.graph, 'text', { 'x': str(2), 'y': str(start - 2), 'class': 'constantLine'}) - text.appendChild(self._doc.createTextNode(label)) - self.graph.appendChild(text) + text.text = label def load_transform_parameters(self): "Cache the parameters necessary to transform x & y coordinates" @@ -308,12 +306,11 @@ class Plot(Graph): and not self.show_data_values: return for ((dx,dy),(gx,gy)) in izip(data_points, graph_points): if self.show_data_points: - circle = self._create_element('circle', { + etree.SubElement(self.graph, 'circle', { 'cx': str(gx), 'cy': str(gy), 'r': '2.5', 'class': 'dataPoint%(line)s' % vars()}) - self.graph.appendChild(circle) if self.show_data_values: self.add_popup(gx, gy, self.format(dx, dy)) self.make_datapoint_text(gx, gy-6, dy) diff --git a/src/svg/charts/schedule.py b/src/svg/charts/schedule.py index 6ae2c30..fdbc7b2 100644 --- a/src/svg/charts/schedule.py +++ b/src/svg/charts/schedule.py @@ -3,6 +3,7 @@ import re from dateutil.parser import parse from dateutil.relativedelta import relativedelta +from lxml import etree from svg.charts.graph import Graph 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_start = scale*(x_start-x_min) - rect = self._create_element('rect', { + etree.SubElement(self.graph, 'rect', { 'x': str(bar_start), 'y': str(y), 'width': str(bar_width), '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):