From 75a928d0eacdfd4ce5a0fbbc41bbf1059be5fc06 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 28 Sep 2011 16:39:46 +0200 Subject: [PATCH] Big cleanup --- pygal/__init__.py | 1 - pygal/bar.py | 81 ++++-------------- pygal/graph.py | 190 ++++++++++++++--------------------------- pygal/line.py | 29 +++---- pygal/pie.py | 57 ++++++------- pygal/plot.py | 161 ++++++++-------------------------- pygal/schedule.py | 135 +++++------------------------ pygal/util/__init__.py | 14 ++- test/tests.py | 2 +- 9 files changed, 185 insertions(+), 485 deletions(-) diff --git a/pygal/__init__.py b/pygal/__init__.py index 6f61823..330974d 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -1,4 +1,3 @@ -#!python # -*- coding: utf-8 -*- """ diff --git a/pygal/bar.py b/pygal/bar.py index d1488c4..f5cca4e 100644 --- a/pygal/bar.py +++ b/pygal/bar.py @@ -1,7 +1,8 @@ -#!python +# -*- coding: utf-8 -*- from itertools import chain from lxml import etree from pygal.graph import Graph +from pygal.util import node __all__ = ('VerticalBar', 'HorizontalBar') @@ -15,7 +16,7 @@ class Bar(Graph): # overlap - overlap bars with transparent colors # top - stack bars on top of one another # side - stack bars side-by-side - stack = 'overlap' + stack = 'side' scale_divisions = None @@ -86,58 +87,7 @@ def float_range(start=0, stop=None, step=1): class VerticalBar(Bar): - """ # === Create presentation quality SVG bar graphs easily - # - # = Synopsis - # - # require 'SVG/Graph/Bar' - # - # fields = %w(Jan Feb Mar); - # data_sales_02 = [12, 45, 21] - # - # graph = SVG::Graph::Bar.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[http://www.w3c.org/tr/svg bar 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, data elements at each point, title, subtitle etc. - # - # = Notes - # - # The default stylesheet handles upto 12 data sets, if you - # use more you must create your own stylesheet and add the - # additional settings for the extra data sets. You will know - # if you go over 12 data sets as they will have no style and - # be in black. - # - # = Examples - # - # * http://germane-software.com/repositories/public/SVG/test/test.rb - # - # = See also - # - # * SVG::Graph::Graph - # * SVG::Graph::BarHorizontal - # * SVG::Graph::Line - # * SVG::Graph::Pie - # * SVG::Graph::Plot - # * SVG::Graph::TimeSeries -""" + """ Vertical bar graph """ top_align = top_font = 1 def get_x_labels(self): @@ -185,13 +135,13 @@ class VerticalBar(Bar): if self.stack == 'side': left += bar_width * dataset_count - rect_group = etree.SubElement(self.graph, "g", + rect_group = node(self.graph, "g", {'class': 'bar'}) - etree.SubElement(rect_group, 'rect', { - 'x': str(left), - 'y': str(top), - 'width': str(bar_width), - 'height': str(length), + node(rect_group, 'rect', { + 'x': left, + 'y': top, + 'width': bar_width, + 'height': length, 'class': 'fill fill%s' % (dataset_count + 1), }) @@ -200,6 +150,7 @@ class VerticalBar(Bar): class HorizontalBar(Bar): + """ Horizontal bar graph """ rotate_y_labels = True show_x_guidelines = True show_y_guidelines = False @@ -246,11 +197,11 @@ class HorizontalBar(Bar): # left is 0 if value is negative left = (abs(min_value) + min(value, 0)) * unit_size - rect = etree.SubElement(self.graph, 'rect', { - 'x': str(left), - 'y': str(top), - 'width': str(length), - 'height': str(bar_height), + node(self.graph, 'rect', { + 'x': left, + 'y': top, + 'width': length, + 'height': bar_height, 'class': 'fill fill%s' % (dataset_count + 1), }) diff --git a/pygal/graph.py b/pygal/graph.py index 7ef8260..15ca6c7 100644 --- a/pygal/graph.py +++ b/pygal/graph.py @@ -1,4 +1,3 @@ -#!python # -*- coding: utf-8 -*- """ @@ -9,20 +8,14 @@ The base module for `pygal` classes. from operator import itemgetter from itertools import islice -import pkg_resources -import functools import os from lxml import etree +from pygal.util import node from pygal.util.boundary import (calculate_right_margin, calculate_left_margin, calculate_bottom_margin, calculate_top_margin, calculate_offsets_bottom) -try: - import zlib -except ImportError: - zlib = None - def sort_multiple(arrays): "sort multiple lists (of equal size) " @@ -36,13 +29,11 @@ class Graph(object): """ Base object for generating SVG Graphs - Synopsis - This class is only used as a superclass of specialized charts. Do not - attempt to use this class directly, unless creating a new chart type. + attempt to use this class directly, unless creating a new chart type. For examples of how to subclass this class, see the existing specific - subclasses, such as svn.charts.Pie. + subclasses, such as svn.charts.Pie. * pygal.bar * pygal.line @@ -169,18 +160,11 @@ class Graph(object): self.draw_data() self.graph.append(self.foreground) - 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 self.compress and zlib: + if self.compress: + import zlib data = zlib.compress(data) return data @@ -193,36 +177,6 @@ class Graph(object): if self.rotate_y_labels: return self.font_size - def add_popup(self, x, y, label): - """ - Add pop-up information to a point on the graph. - """ - txt_width = len(label) * self.font_size * 0.6 + 10 - tx = x + [5, -5][int(x + txt_width > self.width)] - anchor = ['start', 'end'][x + txt_width > self.width] - style = 'fill: #000; text-anchor: %s;' % anchor - id = 'label-%s' % label - t = etree.SubElement(self.foreground, 'text', { - 'x': str(tx), - 'y': str(y - self.font_size), - 'visibility': 'hidden', - 'style': style, - 'text': label, - 'id': id - }) - - # add the circle element to the foreground - visibility = ("document.getElementById('%s')." - "setAttribute('visibility', %%s)" % id) - 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 draw_graph(self): """ The central logic for drawing the graph. @@ -230,23 +184,23 @@ class Graph(object): Sets self.graph (the 'g' element in the SVG root) """ transform = 'translate (%s %s)' % (self.border_left, self.border_top) - self.graph = etree.SubElement(self.root, 'g', transform=transform) + self.graph = node(self.root, 'g', transform=transform) - etree.SubElement(self.graph, 'rect', { - 'x': '0', - 'y': '0', - 'width': str(self.graph_width), - 'height': str(self.graph_height), + node(self.graph, 'rect', { + 'x': 0, + 'y': 0, + 'width': self.graph_width, + 'height': self.graph_height, 'class': 'graphBackground' }) #Axis - etree.SubElement(self.graph, 'path', { + node(self.graph, 'path', { 'd': 'M 0 0 v%s' % self.graph_height, 'class': 'axis', 'id': 'xAxis' }) - etree.SubElement(self.graph, 'path', { + node(self.graph, 'path', { 'd': 'M 0 %s h%s' % (self.graph_height, self.graph_width), 'class': 'axis', 'id': 'yAxis' @@ -255,13 +209,6 @@ class Graph(object): self.draw_x_labels() self.draw_y_labels() - def x_label_offset(self, width): - """ - Return an offset for drawing the x label. Currently returns 0. - """ - # consider width/2 for centering the labels - return 0 - def make_datapoint_text(self, group, x, y, value, style=None): """ Add text for a datapoint @@ -269,9 +216,9 @@ class Graph(object): if not self.show_data_values: return - e = etree.SubElement(group, 'text', { - 'x': str(x), - 'y': str(y), + e = node(group, 'text', { + 'x': x, + 'y': y, 'class': 'dataPointLabel'}) e.text = str(value) if style: @@ -292,19 +239,18 @@ class Graph(object): def draw_x_label(self, label): label_width = self.field_width() index, label = label - text = etree.SubElement(self.graph, 'text', {'class': 'xAxisLabels'}) + text = node(self.graph, 'text', {'class': 'xAxisLabels'}) text.text = label - x = index * label_width + self.x_label_offset(label_width) + x = index * label_width y = self.graph_height + self.x_label_font_size + 3 - t = 0 - (self.font_size / 2) if self.stagger_x_labels and (index % 2): stagger = self.x_label_font_size + 5 y += stagger graph_height = self.graph_height - path = etree.SubElement(self.graph, 'path', { - 'd': 'M%(x)f %(graph_height)f v%(stagger)d' % vars(), + node(self.graph, 'path', { + 'd': 'M%f %f v%d' % (x, graph_height, stagger), 'class': 'staggerGuideLine' }) @@ -341,7 +287,6 @@ class Graph(object): def draw_y_labels(self): "Draw the Y axis labels" if not self.show_y_labels: - # do nothing return labels = self.get_y_labels() @@ -363,7 +308,7 @@ class Graph(object): def draw_y_label(self, label): label_height = self.field_height() index, label = label - text = etree.SubElement(self.graph, 'text', {'class': 'yAxisLabels'}) + text = node(self.graph, 'text', {'class': 'yAxisLabels'}) text.text = label y = self.y_offset - (label_height * index) @@ -372,8 +317,8 @@ class Graph(object): if self.stagger_y_labels and (index % 2): stagger = self.y_label_font_size + 5 x -= stagger - path = etree.SubElement(self.graph, 'path', { - 'd': 'M%(x)f %(y)f h%(stagger)d' % vars(), + path = node(self.graph, 'path', { + 'd': 'M%f %f h%d' % (x, y, stagger), 'class': 'staggerGuideLine' }) @@ -397,8 +342,8 @@ class Graph(object): for count in range(1, count): start = label_height * count stop = self.graph_height - path = etree.SubElement(self.graph, 'path', { - 'd': 'M %(start)s 0 v%(stop)s' % vars(), + node(self.graph, 'path', { + 'd': 'M %s 0 v%s' % (start, stop), 'class': 'guideLines'}) def draw_y_guidelines(self, label_height, count): @@ -408,8 +353,8 @@ class Graph(object): for count in range(1, count): start = self.graph_height - label_height * count stop = self.graph_width - path = etree.SubElement(self.graph, 'path', { - 'd': 'M 0 %(start)s h%(stop)s' % vars(), + node(self.graph, 'path', { + 'd': 'M 0 %s h%s' % (start, stop), 'class': 'guideLines'}) def draw_titles(self): @@ -424,9 +369,9 @@ class Graph(object): self.draw_y_title() def draw_graph_title(self): - text = etree.SubElement(self.root, 'text', { - 'x': str(self.width / 2), - 'y': str(self.title_font_size), + text = node(self.root, 'text', { + 'x': self.width / 2, + 'y': self.title_font_size, 'class': 'mainTitle'}) text.text = self.graph_title @@ -434,9 +379,9 @@ class Graph(object): y_subtitle_options = [self.subtitle_font_size, self.title_font_size + 10] y_subtitle = y_subtitle_options[self.show_graph_title] - text = etree.SubElement(self.root, 'text', { - 'x': str(self.width / 2), - 'y': str(y_subtitle), + text = node(self.root, 'text', { + 'x': self.width / 2, + 'y': y_subtitle, 'class': 'subTitle', }) text.text = self.graph_title @@ -450,9 +395,9 @@ class Graph(object): y += y_size x = self.width / 2 - text = etree.SubElement(self.root, 'text', { - 'x': str(x), - 'y': str(y), + text = node(self.root, 'text', { + 'x': x, + 'y': y, 'class': 'xAxisTitle', }) text.text = self.x_title @@ -466,36 +411,35 @@ class Graph(object): x -= 3 rotate = 90 y = self.height / 2 - text = etree.SubElement(self.root, 'text', { - 'x': str(x), - 'y': str(y), - 'class': 'yAxisTitle', - }) + text = node(self.root, 'text', { + 'x': x, + 'y': y, + 'class': 'yAxisTitle', + }) text.text = self.y_title - text.set('transform', 'rotate(%(rotate)d, %(x)s, %(y)s)' % vars()) + text.set('transform', 'rotate(%d, %s, %s)' % (rotate, x, y)) def keys(self): return map(itemgetter('title'), self.data) def draw_legend(self): if not self.key: - # do nothing return - group = etree.SubElement(self.root, 'g') + group = node(self.root, 'g') for key_count, key_name in enumerate(self.keys()): y_offset = (self.key_box_size * key_count) + (key_count * 5) - etree.SubElement(group, 'rect', { - 'x': '0', - 'y': str(y_offset), - 'width': str(self.key_box_size), - 'height': str(self.key_box_size), + node(group, 'rect', { + 'x': 0, + 'y': y_offset, + 'width': self.key_box_size, + 'height': self.key_box_size, 'class': 'key key%s' % (key_count + 1), }) - text = etree.SubElement(group, 'text', { - 'x': str(self.key_box_size + 5), - 'y': str(y_offset + self.key_box_size), + text = node(group, 'text', { + 'x': self.key_box_size + 5, + 'y': y_offset + self.key_box_size, 'class': 'keyText'}) text.text = key_name @@ -504,7 +448,7 @@ class Graph(object): y_offset = self.border_top + 20 if self.key_position == 'bottom': x_offset, y_offset = calculate_offsets_bottom(self) - group.set('transform', 'translate(%(x_offset)d %(y_offset)d)' % vars()) + group.set('transform', 'translate(%d %d)' % (x_offset, y_offset)) def add_defs(self, defs): """ @@ -513,20 +457,14 @@ class Graph(object): def start_svg(self): "Base SVG Document Creation" - SVG_NAMESPACE = 'http://www.w3.org/2000/svg' - SVG = '{%s}' % SVG_NAMESPACE - NSMAP = { - None: SVG_NAMESPACE, + svg_ns = 'http://www.w3.org/2000/svg' + nsmap = { + None: svg_ns, '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), + self.root = etree.Element("{%s}svg" % svg_ns, attrib={ 'viewBox': '0 0 100% 100%', - # '{http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/}' - # 'scriptImplementation': 'Adobe', - }, nsmap=NSMAP) + }, nsmap=nsmap) if hasattr(self, 'style_sheet_href'): pi = etree.ProcessingInstruction( @@ -541,13 +479,13 @@ class Graph(object): ) map(self.root.append, map(etree.Comment, comment_strings)) - defs = etree.SubElement(self.root, 'defs') + defs = node(self.root, 'defs') self.add_defs(defs) if not hasattr(self, 'style_sheet_href'): self.root.append(etree.Comment( ' include default stylesheet if none specified ')) - style = etree.SubElement(defs, 'style', type='text/css') + style = node(defs, 'style', type='text/css') style.text = '' opts = dict(Graph.__dict__) opts.update(self.__dict__) @@ -558,11 +496,11 @@ class Graph(object): style.text += f.read() % opts 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', + node(self.root, 'rect', { + 'width': self.width, + 'height': self.height, + 'x': 0, + 'y': 0, 'class': 'svgBackground'}) def calculate_graph_dimensions(self): diff --git a/pygal/line.py b/pygal/line.py index 01f076f..c8ba8c1 100644 --- a/pygal/line.py +++ b/pygal/line.py @@ -1,11 +1,7 @@ -#!python - -# $Id$ - +# -*- coding: utf-8 -*- from operator import itemgetter, add -from lxml import etree -from util import flatten, float_range +from pygal.util import node, flatten, float_range from pygal.graph import Graph @@ -136,7 +132,7 @@ class Line(Graph): area_path = ' '.join(paths) origin = paths[-1] else: - area_path = "V%(graph_height)s" % vars(self) + area_path = "V%s" % self.graph_height origin = coord_format(get_coords((0, 0))) d = ' '.join(( @@ -147,32 +143,31 @@ class Line(Graph): area_path, 'Z' )) - etree.SubElement(self.graph, 'path', { - 'class': 'fill%(line_n)s' % vars(), + node(self.graph, 'path', { + 'class': 'fill%s' % line_n, 'd': d, - }) + }) # now draw the line itself - etree.SubElement(self.graph, 'path', { + node(self.graph, 'path', { 'd': 'M0 %s L%s' % (self.graph_height, line_path), - 'class': 'line%(line_n)s' % vars(), + 'class': 'line%s' % line_n, }) if self.show_data_points or self.show_data_values: for i, value in enumerate(cum_sum): if self.show_data_points: - circle = etree.SubElement( + node( self.graph, 'circle', - {'class': 'dataPoint%(line_n)s' % vars()}, + {'class': 'dataPoint%s' % line_n}, cx=str(field_width * i), cy=str(self.graph_height - value * field_height), r='2.5', - ) + ) self.make_datapoint_text( field_width * i, self.graph_height - value * field_height - 6, value + min_value - ) - + ) prev_sum = list(cum_sum) diff --git a/pygal/pie.py b/pygal/pie.py index 33d3b09..b302ee4 100644 --- a/pygal/pie.py +++ b/pygal/pie.py @@ -1,6 +1,6 @@ import math import itertools -from lxml import etree +from pygal.util import node from pygal.graph import Graph @@ -118,14 +118,14 @@ class Pie(Graph): def add_defs(self, defs): "Add svg definitions" - etree.SubElement( + node( defs, 'filter', id='dropshadow', width='1.2', height='1.2', ) - etree.SubElement( + node( defs, 'feGaussianBlur', stdDeviation='4', @@ -158,10 +158,10 @@ class Pie(Graph): return map(key, self.fields, self.data) def draw_data(self): - self.graph = etree.SubElement(self.root, 'g') - background = etree.SubElement(self.graph, 'g') + self.graph = node(self.root, 'g') + background = node(self.graph, 'g') # midground is somewhere between the background and the foreground - midground = etree.SubElement(self.graph, 'g') + midground = node(self.graph, 'g') is_expanded = (self.expanded or self.expand_greatest) diameter = min(self.graph_width, self.graph_height) @@ -174,7 +174,7 @@ class Pie(Graph): 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() + transform = 'translate(%s %s)' % (xoff, yoff) self.graph.set('transform', transform) wedge_text_pad = 5 @@ -198,23 +198,16 @@ class Pie(Graph): 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( + path = "M%s,%s L%s,%s A%s,%s 0, %s, 1, %s %s Z" % ( + radius, radius, x_start, y_start, radius, radius, + percent_greater_fifty, x_end, y_end) + + wedge = node( self.foreground, - 'path', - { + 'path', { 'd': path, - 'class': 'fill%s' % (index + 1), - } - ) + 'class': 'fill%s' % (index + 1)} + ) translate = None tx = 0 @@ -223,14 +216,14 @@ class Pie(Graph): radians = half_percent * rad_mult if self.show_shadow: - shadow = etree.SubElement( + shadow = node( background, 'path', d=path, filter='url(#dropshadow)', style='fill: #ccc; stroke: none', ) - clear = etree.SubElement( + clear = node( midground, 'path', d=path, @@ -243,14 +236,14 @@ class Pie(Graph): 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() + translate = "translate(%s %s)" % (tx, ty) 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() + translate = 'translate(%s %s)' % (shadow_tx, shadow_ty) shadow.set('transform', translate) if self.show_data_labels and value != 0: @@ -273,24 +266,24 @@ class Pie(Graph): tx += (msr * self.expand_gap) ty -= (mcr * self.expand_gap) - label_node = etree.SubElement( + label_node = node( self.foreground, 'text', { - 'x': str(tx), - 'y': str(ty), + 'x': tx, + 'y': ty, 'class': 'dataPointLabel', 'style': 'stroke: #fff; stroke-width: 2;' } ) label_node.text = label - label_node = etree.SubElement( + label_node = node( self.foreground, 'text', { - 'x': str(tx), - 'y': str(ty), + 'x': tx, + 'y': ty, 'class': 'dataPointLabel', } ) diff --git a/pygal/plot.py b/pygal/plot.py index 34315f7..2959f45 100644 --- a/pygal/plot.py +++ b/pygal/plot.py @@ -6,10 +6,9 @@ import sys from itertools import izip, count, chain from lxml import etree +from pygal.util import node, float_range from pygal.graph import Graph -from .util import float_range - def get_pairs(i): i = iter(i) @@ -23,116 +22,33 @@ if sys.version >= '3': class Plot(Graph): - """=== For creating SVG plots of scalar data - - = Synopsis - - require 'SVG/Graph/Plot' - - # Data sets are x,y pairs - # Note that multiple data sets can differ in length, and that the - # data in the datasets needn't be in order; they will be ordered - # by the plot along the X-axis. - projection = [ - 6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13, - 7, 9 - ] - actual = [ - 0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12, - 15, 6, 4, 17, 2, 12 - ] - - graph = SVG::Graph::Plot.new({ - :height => 500, - :width => 300, - :key => true, - :scale_x_integers => true, - :scale_y_integerrs => true, - }) - - graph.add_data({ - :data => projection - :title => 'Projected', - }) - - graph.add_data({ - :data => actual, - :title => 'Actual', - }) - - print graph.burn() - - = Description - - Produces a graph of scalar data. - - This object aims to allow you to easily create high quality - SVG[http://www.w3c.org/tr/svg] scalar plots. 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, data elements at each point, title, - subtitle etc. - - = Examples - - http://www.germane-software/repositories/public/SVG/test/plot.rb - - = Notes - - The default stylesheet handles upto 10 data sets, if you - use more you must create your own stylesheet and add the - additional settings for the extra data sets. You will know - if you go over 10 data sets as they will have no style and - be in black. - - Unlike the other types of charts, data sets must contain x,y pairs: - - [1, 2] # A data set with 1 point: (1,2) - [1,2, 5,6] # A data set with 2 points: (1,2) and (5,6) - - = See also - - * SVG::Graph::Graph - * SVG::Graph::BarHorizontal - * SVG::Graph::Bar - * SVG::Graph::Line - * SVG::Graph::Pie - * SVG::Graph::TimeSeries - - == Author - - Sean E. Russell - - Copyright 2004 Sean E. Russell - This software is available under the Ruby license[LICENSE.txt]""" + """Graph of scalar data.""" top_align = right_align = top_font = right_font = 1 - """Determines the scaling for the Y axis divisions. - - graph.scale_y_divisions = 0.5 - - would cause the graph to attempt to generate labels stepped by 0.5; EG: - 0, 0.5, 1, 1.5, 2, ...""" + # Determines the scaling for the Y axis divisions. + # graph.scale_y_divisions = 0.5 + # would cause the graph to attempt to generate labels stepped by 0.5; EG: + # 0, 0.5, 1, 1.5, 2, ... scale_y_divisions = None - "Make the X axis labels integers" + # Make the X axis labels integers scale_x_integers = False - "Make the Y axis labels integers" + # Make the Y axis labels integers scale_y_integers = False - "Fill the area under the line" + # Fill the area under the line area_fill = False - """Show a small circle on the graph where the line - goes from one point to the next.""" + # Show a small circle on the graph where the line + # goes from one point to the next. show_data_points = True - "Indicate whether the lines should be drawn between points" + # Indicate whether the lines should be drawn between points draw_lines_between_points = True - "Set the minimum value of the X axis" + # Set the minimum value of the X axis min_x_value = None - "Set the minimum value of the Y axis" + # Set the minimum value of the Y axis min_y_value = None - "Set the maximum value of the X axis" + # Set the maximum value of the X axis max_x_value = None - "Set the maximum value of the Y axis" + # Set the maximum value of the Y axis max_y_value = None stacked = False @@ -141,13 +57,12 @@ class Plot(Graph): @apply def scale_x_divisions(): - doc = """Determines the scaling for the X axis divisions. - - graph.scale_x_divisions = 2 + """Determines the scaling for the X axis divisions. - would cause the graph to attempt - to generate labels stepped by 2; EG: - 0,2,4,6,8...""" + graph.scale_x_divisions = 2 + would cause the graph to attempt + to generate labels stepped by 2; EG: + 0,2,4,6,8...""" def fget(self): return getattr(self, '_scale_x_divisions', None) @@ -279,14 +194,14 @@ class Plot(Graph): lpath = self.get_lpath(graph_points) if self.area_fill: graph_height = self.graph_height - 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()}) + node(self.graph, 'path', { + 'd': 'M%f %f %s V%f Z' % ( + x_start, graph_height, lpath, graph_height), + 'class': 'fill%d' % line}) if self.draw_lines_between_points: - path = etree.SubElement(self.graph, 'path', { - 'd': 'M%(x_start)f %(y_start)f %(lpath)s' % vars(), - 'class': 'line%(line)d' % vars()}) + node(self.graph, 'path', { + 'd': 'M%f %f %s' % (x_start, y_start, lpath), + 'class': 'line%d' % line}) self.draw_data_points(line, data_points, graph_points) self._draw_constant_lines() del self.__transform_parameters @@ -304,14 +219,14 @@ class Plot(Graph): value, label, style = value_label_style start = self.transform_output_coordinates((0, value))[1] stop = self.graph_width - path = etree.SubElement(self.graph, 'path', { - 'd': 'M 0 %(start)s h%(stop)s' % vars(), + path = node(self.graph, 'path', { + 'd': 'M 0 %s h%s' % (start, stop), 'class': 'constantLine'}) if style: path.set('style', style) - text = etree.SubElement(self.graph, 'text', { - 'x': str(2), - 'y': str(start - 2), + text = node(self.graph, 'text', { + 'x': 2, + 'y': start - 2, 'class': 'constantLine'}) text.text = label @@ -338,8 +253,6 @@ class Plot(Graph): x_step = self.__transform_parameters['x_step'] y_min = self.__transform_parameters['y_min'] y_step = self.__transform_parameters['y_step'] - #locals().update(self.__transform_parameters) - #vars().update(self.__transform_parameters) x = (x - x_min) * x_step y = self.graph_height - (y - y_min) * y_step return x, y @@ -350,11 +263,11 @@ class Plot(Graph): for ((dx, dy), (gx, gy)) in izip(data_points, graph_points): if self.show_data_points: - etree.SubElement(self.graph, 'circle', { - 'cx': str(gx), - 'cy': str(gy), + node(self.graph, 'circle', { + 'cx': gx, + 'cy': gy, 'r': '2.5', - 'class': 'dataPoint%(line)s' % vars()}) + 'class': 'dataPoint%s' % line}) 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/pygal/schedule.py b/pygal/schedule.py index 1189620..535a334 100644 --- a/pygal/schedule.py +++ b/pygal/schedule.py @@ -1,115 +1,36 @@ -#!python +# -*- coding: utf-8 -*- import re from dateutil.parser import parse from dateutil.relativedelta import relativedelta from lxml import etree +from pygal.util import (node, grouper, date_range, + divide_timedelta_float, TimeScale) from pygal.graph import Graph -from util import grouper, date_range, divide_timedelta_float, TimeScale __all__ = ('Schedule') class Schedule(Graph): """ - # === For creating SVG plots of scalar temporal data - - = Synopsis - - require 'SVG/Graph/Schedule' - - # Data sets are label, start, end tripples. - data1 = [ - "Housesitting", "6/17/04", "6/19/04", - "Summer Session", "6/15/04", "8/15/04", - ] - - graph = SVG::Graph::Schedule.new( { - :width => 640, - :height => 480, - :graph_title => title, - :show_graph_title => true, - :no_css => true, - :scale_x_integers => true, - :scale_y_integers => true, - :min_x_value => 0, - :min_y_value => 0, - :show_data_labels => true, - :show_x_guidelines => true, - :show_x_title => true, - :x_title => "Time", - :stagger_x_labels => true, - :stagger_y_labels => true, - :x_label_format => "%m/%d/%y", - }) - - graph.add_data({ - :data => data1, - :title => 'Data', - }) - - print graph.burn() - - = Description - - Produces a graph of temporal scalar data. - - = Examples - - http://www.germane-software/repositories/public/SVG/test/schedule.rb - - = Notes - - The default stylesheet handles upto 10 data sets, if you - use more you must create your own stylesheet and add the - additional settings for the extra data sets. You will know - if you go over 10 data sets as they will have no style and - be in black. - - Note that multiple data sets within the same chart can differ in - length, and that the data in the datasets needn't be in order; - they will be ordered by the plot along the X-axis. - - The dates must be parseable by ParseDate, but otherwise can be - any order of magnitude (seconds within the hour, or years) - - = See also - - * SVG::Graph::Graph - * SVG::Graph::BarHorizontal - * SVG::Graph::Bar - * SVG::Graph::Line - * SVG::Graph::Pie - * 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] - + Graph of temporal scalar data. """ - "The format string to be used to format the X axis labels" + # The format string to be used to format the X axis labels x_label_format = '%Y-%m-%d %H:%M:%S' - """ - Use this to set the spacing between dates on the axis. The value - must be of the form - "\d+ ?((year|month|week|day|hour|minute|second)s?)?" - - e.g. - - graph.timescale_divisions = '2 weeks' - graph.timescale_divisions = '1 month' - graph.timescale_divisions = '3600 seconds' # easier would be '1 hour' - """ + # Use this to set the spacing between dates on the axis. The value + # must be of the form + # "\d+ ?((year|month|week|day|hour|minute|second)s?)?" + # e.g. + # graph.timescale_divisions = '2 weeks' + # graph.timescale_divisions = '1 month' + # graph.timescale_divisions = '3600 seconds' + # easier would be '1 hour' timescale_divisions = None - "The formatting used for the popups. See x_label_format" + # The formatting used for the popups. See x_label_format popup_format = '%Y-%m-%d %H:%M:%S' _min_x_value = None @@ -122,23 +43,6 @@ class Schedule(Graph): def add_data(self, data): """ Add data to the plot. - - # A data set with 1 point: Lunch from 12:30 to 14:00 - d1 = [ "Lunch", "12:30", "14:00" ] - # A data set with 2 points: "Cats" runs from 5/11/03 to 7/15/04, and - # "Henry V" runs from 6/12/03 to 8/20/03 - d2 = [ "Cats", "5/11/03", "7/15/04", - "Henry V", "6/12/03", "8/20/03" ] - - graph.add_data( - :data => d1, - :title => 'Meetings' - ) - graph.add_data( - :data => d2, - :title => 'Plays' - ) - Note that the data must be in time,value pairs, and that the date format may be any date that is parseable by ParseDate. @@ -220,7 +124,6 @@ class Schedule(Graph): subbar_height = self.get_field_height() - bar_gap - y_mod = (subbar_height / 2) + (self.font_size / 2) x_min, x_max, div = self._x_range() x_range = x_max - x_min width = (float(self.graph_width) - self.font_size * 2) @@ -238,11 +141,11 @@ class Schedule(Graph): bar_width = scale * (x_end - x_start) bar_start = scale * (x_start - x_min) - etree.SubElement(self.graph, 'rect', { - 'x': str(bar_start), - 'y': str(y), - 'width': str(bar_width), - 'height': str(subbar_height), + node(self.graph, 'rect', { + 'x': bar_start, + 'y': y, + 'width': bar_width, + 'height': subbar_height, 'class': 'fill%s' % (count + 1), }) diff --git a/pygal/util/__init__.py b/pygal/util/__init__.py index e779bd7..2d5d7b5 100644 --- a/pygal/util/__init__.py +++ b/pygal/util/__init__.py @@ -1,8 +1,16 @@ -#!python - +# -*- coding: utf-8 -*- +from lxml import etree import itertools import datetime -# from itertools recipes (python documentation) + + +def node(parent, tag, params={}, **extras): + """Make a etree node""" + for key, value in params.items(): + if not isinstance(value, basestring): + params[key] = str(value) + + return etree.SubElement(parent, tag, params, **extras) def grouper(n, iterable, padvalue=None): diff --git a/test/tests.py b/test/tests.py index e70b44d..ce61486 100755 --- a/test/tests.py +++ b/test/tests.py @@ -5,4 +5,4 @@ from moulinrouge import create_app app = create_app() if __name__ == "__main__": - app.run(debug=True, port=21112) + app.run(debug=True, threaded=True, port=21112)