Browse Source

Big cleanup

pull/8/head
Florian Mounier 13 years ago
parent
commit
75a928d0ea
  1. 1
      pygal/__init__.py
  2. 81
      pygal/bar.py
  3. 190
      pygal/graph.py
  4. 29
      pygal/line.py
  5. 57
      pygal/pie.py
  6. 161
      pygal/plot.py
  7. 135
      pygal/schedule.py
  8. 14
      pygal/util/__init__.py
  9. 2
      test/tests.py

1
pygal/__init__.py

@ -1,4 +1,3 @@
#!python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """

81
pygal/bar.py

@ -1,7 +1,8 @@
#!python # -*- coding: utf-8 -*-
from itertools import chain from itertools import chain
from lxml import etree from lxml import etree
from pygal.graph import Graph from pygal.graph import Graph
from pygal.util import node
__all__ = ('VerticalBar', 'HorizontalBar') __all__ = ('VerticalBar', 'HorizontalBar')
@ -15,7 +16,7 @@ class Bar(Graph):
# overlap - overlap bars with transparent colors # overlap - overlap bars with transparent colors
# top - stack bars on top of one another # top - stack bars on top of one another
# side - stack bars side-by-side # side - stack bars side-by-side
stack = 'overlap' stack = 'side'
scale_divisions = None scale_divisions = None
@ -86,58 +87,7 @@ def float_range(start=0, stop=None, step=1):
class VerticalBar(Bar): class VerticalBar(Bar):
""" # === Create presentation quality SVG bar graphs easily """ Vertical bar graph """
#
# = 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
"""
top_align = top_font = 1 top_align = top_font = 1
def get_x_labels(self): def get_x_labels(self):
@ -185,13 +135,13 @@ class VerticalBar(Bar):
if self.stack == 'side': if self.stack == 'side':
left += bar_width * dataset_count left += bar_width * dataset_count
rect_group = etree.SubElement(self.graph, "g", rect_group = node(self.graph, "g",
{'class': 'bar'}) {'class': 'bar'})
etree.SubElement(rect_group, 'rect', { node(rect_group, 'rect', {
'x': str(left), 'x': left,
'y': str(top), 'y': top,
'width': str(bar_width), 'width': bar_width,
'height': str(length), 'height': length,
'class': 'fill fill%s' % (dataset_count + 1), 'class': 'fill fill%s' % (dataset_count + 1),
}) })
@ -200,6 +150,7 @@ class VerticalBar(Bar):
class HorizontalBar(Bar): class HorizontalBar(Bar):
""" Horizontal bar graph """
rotate_y_labels = True rotate_y_labels = True
show_x_guidelines = True show_x_guidelines = True
show_y_guidelines = False show_y_guidelines = False
@ -246,11 +197,11 @@ 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 = etree.SubElement(self.graph, 'rect', { node(self.graph, 'rect', {
'x': str(left), 'x': left,
'y': str(top), 'y': top,
'width': str(length), 'width': length,
'height': str(bar_height), 'height': bar_height,
'class': 'fill fill%s' % (dataset_count + 1), 'class': 'fill fill%s' % (dataset_count + 1),
}) })

190
pygal/graph.py

@ -1,4 +1,3 @@
#!python
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
@ -9,20 +8,14 @@ The base module for `pygal` classes.
from operator import itemgetter from operator import itemgetter
from itertools import islice from itertools import islice
import pkg_resources
import functools
import os import os
from lxml import etree from lxml import etree
from pygal.util import node
from pygal.util.boundary import (calculate_right_margin, calculate_left_margin, from pygal.util.boundary import (calculate_right_margin, calculate_left_margin,
calculate_bottom_margin, calculate_top_margin, calculate_bottom_margin, calculate_top_margin,
calculate_offsets_bottom) calculate_offsets_bottom)
try:
import zlib
except ImportError:
zlib = None
def sort_multiple(arrays): def sort_multiple(arrays):
"sort multiple lists (of equal size) " "sort multiple lists (of equal size) "
@ -36,13 +29,11 @@ class Graph(object):
""" """
Base object for generating SVG Graphs Base object for generating SVG Graphs
Synopsis
This class is only used as a superclass of specialized charts. Do not 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 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.bar
* pygal.line * pygal.line
@ -169,18 +160,11 @@ class Graph(object):
self.draw_data() self.draw_data()
self.graph.append(self.foreground) 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( data = etree.tostring(
self.root, pretty_print=True, self.root, pretty_print=True,
xml_declaration=True, encoding='utf-8') xml_declaration=True, encoding='utf-8')
if self.compress:
if self.compress and zlib: import zlib
data = zlib.compress(data) data = zlib.compress(data)
return data return data
@ -193,36 +177,6 @@ class Graph(object):
if self.rotate_y_labels: if self.rotate_y_labels:
return self.font_size 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): def draw_graph(self):
""" """
The central logic for drawing the graph. The central logic for drawing the graph.
@ -230,23 +184,23 @@ class Graph(object):
Sets self.graph (the 'g' element in the SVG root) Sets self.graph (the 'g' element in the SVG root)
""" """
transform = 'translate (%s %s)' % (self.border_left, self.border_top) 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', { node(self.graph, 'rect', {
'x': '0', 'x': 0,
'y': '0', 'y': 0,
'width': str(self.graph_width), 'width': self.graph_width,
'height': str(self.graph_height), 'height': self.graph_height,
'class': 'graphBackground' 'class': 'graphBackground'
}) })
#Axis #Axis
etree.SubElement(self.graph, 'path', { node(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'
}) })
etree.SubElement(self.graph, 'path', { node(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'
@ -255,13 +209,6 @@ class Graph(object):
self.draw_x_labels() self.draw_x_labels()
self.draw_y_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): def make_datapoint_text(self, group, x, y, value, style=None):
""" """
Add text for a datapoint Add text for a datapoint
@ -269,9 +216,9 @@ class Graph(object):
if not self.show_data_values: if not self.show_data_values:
return return
e = etree.SubElement(group, 'text', { e = node(group, 'text', {
'x': str(x), 'x': x,
'y': str(y), 'y': y,
'class': 'dataPointLabel'}) 'class': 'dataPointLabel'})
e.text = str(value) e.text = str(value)
if style: if style:
@ -292,19 +239,18 @@ 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 = etree.SubElement(self.graph, 'text', {'class': 'xAxisLabels'}) text = node(self.graph, 'text', {'class': 'xAxisLabels'})
text.text = label 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 y = self.graph_height + self.x_label_font_size + 3
t = 0 - (self.font_size / 2)
if self.stagger_x_labels and (index % 2): if self.stagger_x_labels and (index % 2):
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 = etree.SubElement(self.graph, 'path', { node(self.graph, 'path', {
'd': 'M%(x)f %(graph_height)f v%(stagger)d' % vars(), 'd': 'M%f %f v%d' % (x, graph_height, stagger),
'class': 'staggerGuideLine' 'class': 'staggerGuideLine'
}) })
@ -341,7 +287,6 @@ class Graph(object):
def draw_y_labels(self): def draw_y_labels(self):
"Draw the Y axis labels" "Draw the Y axis labels"
if not self.show_y_labels: if not self.show_y_labels:
# do nothing
return return
labels = self.get_y_labels() labels = self.get_y_labels()
@ -363,7 +308,7 @@ 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 = etree.SubElement(self.graph, 'text', {'class': 'yAxisLabels'}) text = node(self.graph, 'text', {'class': 'yAxisLabels'})
text.text = label text.text = label
y = self.y_offset - (label_height * index) y = self.y_offset - (label_height * index)
@ -372,8 +317,8 @@ 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 = etree.SubElement(self.graph, 'path', { path = node(self.graph, 'path', {
'd': 'M%(x)f %(y)f h%(stagger)d' % vars(), 'd': 'M%f %f h%d' % (x, y, stagger),
'class': 'staggerGuideLine' 'class': 'staggerGuideLine'
}) })
@ -397,8 +342,8 @@ 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 = etree.SubElement(self.graph, 'path', { node(self.graph, 'path', {
'd': 'M %(start)s 0 v%(stop)s' % vars(), 'd': 'M %s 0 v%s' % (start, stop),
'class': 'guideLines'}) 'class': 'guideLines'})
def draw_y_guidelines(self, label_height, count): def draw_y_guidelines(self, label_height, count):
@ -408,8 +353,8 @@ 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 = etree.SubElement(self.graph, 'path', { node(self.graph, 'path', {
'd': 'M 0 %(start)s h%(stop)s' % vars(), 'd': 'M 0 %s h%s' % (start, stop),
'class': 'guideLines'}) 'class': 'guideLines'})
def draw_titles(self): def draw_titles(self):
@ -424,9 +369,9 @@ class Graph(object):
self.draw_y_title() self.draw_y_title()
def draw_graph_title(self): def draw_graph_title(self):
text = etree.SubElement(self.root, 'text', { text = node(self.root, 'text', {
'x': str(self.width / 2), 'x': self.width / 2,
'y': str(self.title_font_size), 'y': self.title_font_size,
'class': 'mainTitle'}) 'class': 'mainTitle'})
text.text = self.graph_title text.text = self.graph_title
@ -434,9 +379,9 @@ class Graph(object):
y_subtitle_options = [self.subtitle_font_size, y_subtitle_options = [self.subtitle_font_size,
self.title_font_size + 10] self.title_font_size + 10]
y_subtitle = y_subtitle_options[self.show_graph_title] y_subtitle = y_subtitle_options[self.show_graph_title]
text = etree.SubElement(self.root, 'text', { text = node(self.root, 'text', {
'x': str(self.width / 2), 'x': self.width / 2,
'y': str(y_subtitle), 'y': y_subtitle,
'class': 'subTitle', 'class': 'subTitle',
}) })
text.text = self.graph_title text.text = self.graph_title
@ -450,9 +395,9 @@ class Graph(object):
y += y_size y += y_size
x = self.width / 2 x = self.width / 2
text = etree.SubElement(self.root, 'text', { text = node(self.root, 'text', {
'x': str(x), 'x': x,
'y': str(y), 'y': y,
'class': 'xAxisTitle', 'class': 'xAxisTitle',
}) })
text.text = self.x_title text.text = self.x_title
@ -466,36 +411,35 @@ class Graph(object):
x -= 3 x -= 3
rotate = 90 rotate = 90
y = self.height / 2 y = self.height / 2
text = etree.SubElement(self.root, 'text', { text = node(self.root, 'text', {
'x': str(x), 'x': x,
'y': str(y), 'y': y,
'class': 'yAxisTitle', 'class': 'yAxisTitle',
}) })
text.text = self.y_title 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): def keys(self):
return map(itemgetter('title'), self.data) return map(itemgetter('title'), self.data)
def draw_legend(self): def draw_legend(self):
if not self.key: if not self.key:
# do nothing
return return
group = etree.SubElement(self.root, 'g') group = node(self.root, 'g')
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)
etree.SubElement(group, 'rect', { node(group, 'rect', {
'x': '0', 'x': 0,
'y': str(y_offset), 'y': y_offset,
'width': str(self.key_box_size), 'width': self.key_box_size,
'height': str(self.key_box_size), 'height': self.key_box_size,
'class': 'key key%s' % (key_count + 1), 'class': 'key key%s' % (key_count + 1),
}) })
text = etree.SubElement(group, 'text', { text = node(group, 'text', {
'x': str(self.key_box_size + 5), 'x': self.key_box_size + 5,
'y': str(y_offset + self.key_box_size), 'y': y_offset + self.key_box_size,
'class': 'keyText'}) 'class': 'keyText'})
text.text = key_name text.text = key_name
@ -504,7 +448,7 @@ class Graph(object):
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 = calculate_offsets_bottom(self) 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): def add_defs(self, defs):
""" """
@ -513,20 +457,14 @@ class Graph(object):
def start_svg(self): def start_svg(self):
"Base SVG Document Creation" "Base SVG Document Creation"
SVG_NAMESPACE = 'http://www.w3.org/2000/svg' svg_ns = 'http://www.w3.org/2000/svg'
SVG = '{%s}' % SVG_NAMESPACE nsmap = {
NSMAP = { None: svg_ns,
None: SVG_NAMESPACE,
'xlink': 'http://www.w3.org/1999/xlink', 'xlink': 'http://www.w3.org/1999/xlink',
# 'a3': 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/',
} }
self.root = etree.Element(SVG + "svg", attrib={ self.root = etree.Element("{%s}svg" % svg_ns, attrib={
# 'width': str(self.width),
# 'height': str(self.height),
'viewBox': '0 0 100% 100%', 'viewBox': '0 0 100% 100%',
# '{http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/}' }, nsmap=nsmap)
# 'scriptImplementation': 'Adobe',
}, nsmap=NSMAP)
if hasattr(self, 'style_sheet_href'): if hasattr(self, 'style_sheet_href'):
pi = etree.ProcessingInstruction( pi = etree.ProcessingInstruction(
@ -541,13 +479,13 @@ class Graph(object):
) )
map(self.root.append, map(etree.Comment, comment_strings)) map(self.root.append, map(etree.Comment, comment_strings))
defs = etree.SubElement(self.root, 'defs') defs = node(self.root, 'defs')
self.add_defs(defs) self.add_defs(defs)
if not hasattr(self, 'style_sheet_href'): if not hasattr(self, 'style_sheet_href'):
self.root.append(etree.Comment( self.root.append(etree.Comment(
' include default stylesheet if none specified ')) ' include default stylesheet if none specified '))
style = etree.SubElement(defs, 'style', type='text/css') style = node(defs, 'style', type='text/css')
style.text = '' style.text = ''
opts = dict(Graph.__dict__) opts = dict(Graph.__dict__)
opts.update(self.__dict__) opts.update(self.__dict__)
@ -558,11 +496,11 @@ class Graph(object):
style.text += f.read() % opts style.text += f.read() % opts
self.root.append(etree.Comment('SVG Background')) self.root.append(etree.Comment('SVG Background'))
rect = etree.SubElement(self.root, 'rect', { node(self.root, 'rect', {
'width': str(self.width), 'width': self.width,
'height': str(self.height), 'height': self.height,
'x': '0', 'x': 0,
'y': '0', 'y': 0,
'class': 'svgBackground'}) 'class': 'svgBackground'})
def calculate_graph_dimensions(self): def calculate_graph_dimensions(self):

29
pygal/line.py

@ -1,11 +1,7 @@
#!python # -*- coding: utf-8 -*-
# $Id$
from operator import itemgetter, add 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 from pygal.graph import Graph
@ -136,7 +132,7 @@ class Line(Graph):
area_path = ' '.join(paths) area_path = ' '.join(paths)
origin = paths[-1] origin = paths[-1]
else: else:
area_path = "V%(graph_height)s" % vars(self) area_path = "V%s" % self.graph_height
origin = coord_format(get_coords((0, 0))) origin = coord_format(get_coords((0, 0)))
d = ' '.join(( d = ' '.join((
@ -147,32 +143,31 @@ class Line(Graph):
area_path, area_path,
'Z' 'Z'
)) ))
etree.SubElement(self.graph, 'path', { node(self.graph, 'path', {
'class': 'fill%(line_n)s' % vars(), 'class': 'fill%s' % line_n,
'd': d, 'd': d,
}) })
# now draw the line itself # now draw the line itself
etree.SubElement(self.graph, 'path', { node(self.graph, 'path', {
'd': 'M0 %s L%s' % (self.graph_height, line_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: 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 = etree.SubElement( node(
self.graph, self.graph,
'circle', 'circle',
{'class': 'dataPoint%(line_n)s' % vars()}, {'class': 'dataPoint%s' % line_n},
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',
) )
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,
value + min_value value + min_value
) )
prev_sum = list(cum_sum) prev_sum = list(cum_sum)

57
pygal/pie.py

@ -1,6 +1,6 @@
import math import math
import itertools import itertools
from lxml import etree from pygal.util import node
from pygal.graph import Graph from pygal.graph import Graph
@ -118,14 +118,14 @@ class Pie(Graph):
def add_defs(self, defs): def add_defs(self, defs):
"Add svg definitions" "Add svg definitions"
etree.SubElement( node(
defs, defs,
'filter', 'filter',
id='dropshadow', id='dropshadow',
width='1.2', width='1.2',
height='1.2', height='1.2',
) )
etree.SubElement( node(
defs, defs,
'feGaussianBlur', 'feGaussianBlur',
stdDeviation='4', stdDeviation='4',
@ -158,10 +158,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 = etree.SubElement(self.root, 'g') self.graph = node(self.root, 'g')
background = etree.SubElement(self.graph, 'g') background = node(self.graph, 'g')
# midground is somewhere between the background and the foreground # 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) is_expanded = (self.expanded or self.expand_greatest)
diameter = min(self.graph_width, self.graph_height) diameter = min(self.graph_width, self.graph_height)
@ -174,7 +174,7 @@ class Pie(Graph):
xoff = (self.width - diameter) / 2 xoff = (self.width - diameter) / 2
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(%s %s)' % (xoff, yoff)
self.graph.set('transform', transform) self.graph.set('transform', transform)
wedge_text_pad = 5 wedge_text_pad = 5
@ -198,23 +198,16 @@ class Pie(Graph):
x_end = radius + (math.sin(radians) * radius) x_end = radius + (math.sin(radians) * radius)
y_end = radius - (math.cos(radians) * radius) y_end = radius - (math.cos(radians) * radius)
percent_greater_fifty = int(percent >= 50) percent_greater_fifty = int(percent >= 50)
path = ' '.join(( path = "M%s,%s L%s,%s A%s,%s 0, %s, 1, %s %s Z" % (
"M%(radius)s,%(radius)s", radius, radius, x_start, y_start, radius, radius,
"L%(x_start)s,%(y_start)s", percent_greater_fifty, x_end, y_end)
"A%(radius)s,%(radius)s",
"0,", wedge = node(
"%(percent_greater_fifty)s,1,",
"%(x_end)s %(y_end)s Z"))
path = path % vars()
wedge = etree.SubElement(
self.foreground, self.foreground,
'path', 'path', {
{
'd': path, 'd': path,
'class': 'fill%s' % (index + 1), 'class': 'fill%s' % (index + 1)}
} )
)
translate = None translate = None
tx = 0 tx = 0
@ -223,14 +216,14 @@ class Pie(Graph):
radians = half_percent * rad_mult radians = half_percent * rad_mult
if self.show_shadow: if self.show_shadow:
shadow = etree.SubElement( shadow = node(
background, background,
'path', 'path',
d=path, d=path,
filter='url(#dropshadow)', filter='url(#dropshadow)',
style='fill: #ccc; stroke: none', style='fill: #ccc; stroke: none',
) )
clear = etree.SubElement( clear = node(
midground, midground,
'path', 'path',
d=path, d=path,
@ -243,14 +236,14 @@ class Pie(Graph):
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(%s %s)" % (tx, ty)
wedge.set('transform', translate) wedge.set('transform', translate)
clear.set('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(%s %s)' % (shadow_tx, shadow_ty)
shadow.set('transform', translate) shadow.set('transform', translate)
if self.show_data_labels and value != 0: if self.show_data_labels and value != 0:
@ -273,24 +266,24 @@ 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 = etree.SubElement( label_node = node(
self.foreground, self.foreground,
'text', 'text',
{ {
'x': str(tx), 'x': tx,
'y': str(ty), 'y': ty,
'class': 'dataPointLabel', 'class': 'dataPointLabel',
'style': 'stroke: #fff; stroke-width: 2;' 'style': 'stroke: #fff; stroke-width: 2;'
} }
) )
label_node.text = label label_node.text = label
label_node = etree.SubElement( label_node = node(
self.foreground, self.foreground,
'text', 'text',
{ {
'x': str(tx), 'x': tx,
'y': str(ty), 'y': ty,
'class': 'dataPointLabel', 'class': 'dataPointLabel',
} }
) )

161
pygal/plot.py

@ -6,10 +6,9 @@ import sys
from itertools import izip, count, chain from itertools import izip, count, chain
from lxml import etree from lxml import etree
from pygal.util import node, float_range
from pygal.graph import Graph from pygal.graph import Graph
from .util import float_range
def get_pairs(i): def get_pairs(i):
i = iter(i) i = iter(i)
@ -23,116 +22,33 @@ if sys.version >= '3':
class Plot(Graph): class Plot(Graph):
"""=== For creating SVG plots of scalar data """Graph 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 <serATgermaneHYPHENsoftwareDOTcom>
Copyright 2004 Sean E. Russell
This software is available under the Ruby license[LICENSE.txt]"""
top_align = right_align = top_font = right_font = 1 top_align = right_align = top_font = right_font = 1
"""Determines the scaling for the Y axis divisions. # Determines the scaling for the Y axis divisions.
# graph.scale_y_divisions = 0.5
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, ...
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 scale_y_divisions = None
"Make the X axis labels integers" # Make the X axis labels integers
scale_x_integers = False scale_x_integers = False
"Make the Y axis labels integers" # Make the Y axis labels integers
scale_y_integers = False scale_y_integers = False
"Fill the area under the line" # Fill the area under the line
area_fill = False area_fill = False
"""Show a small circle on the graph where the line # Show a small circle on the graph where the line
goes from one point to the next.""" # goes from one point to the next.
show_data_points = True 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 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 min_x_value = None
"Set the minimum value of the Y axis" # Set the minimum value of the Y axis
min_y_value = None min_y_value = None
"Set the maximum value of the X axis" # Set the maximum value of the X axis
max_x_value = None max_x_value = None
"Set the maximum value of the Y axis" # Set the maximum value of the Y axis
max_y_value = None max_y_value = None
stacked = False stacked = False
@ -141,13 +57,12 @@ class Plot(Graph):
@apply @apply
def scale_x_divisions(): def scale_x_divisions():
doc = """Determines the scaling for the X axis divisions. """Determines the scaling for the X axis divisions.
graph.scale_x_divisions = 2
would cause the graph to attempt graph.scale_x_divisions = 2
to generate labels stepped by 2; EG: would cause the graph to attempt
0,2,4,6,8...""" to generate labels stepped by 2; EG:
0,2,4,6,8..."""
def fget(self): def fget(self):
return getattr(self, '_scale_x_divisions', None) return getattr(self, '_scale_x_divisions', None)
@ -279,14 +194,14 @@ 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 = etree.SubElement(self.graph, 'path', { node(self.graph, 'path', {
'd': 'M%(x_start)f %(graph_height)f' 'd': 'M%f %f %s V%f Z' % (
' %(lpath)s V%(graph_height)f Z' % vars(), x_start, graph_height, lpath, graph_height),
'class': 'fill%(line)d' % vars()}) 'class': 'fill%d' % line})
if self.draw_lines_between_points: if self.draw_lines_between_points:
path = etree.SubElement(self.graph, 'path', { node(self.graph, 'path', {
'd': 'M%(x_start)f %(y_start)f %(lpath)s' % vars(), 'd': 'M%f %f %s' % (x_start, y_start, lpath),
'class': 'line%(line)d' % vars()}) 'class': 'line%d' % line})
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
@ -304,14 +219,14 @@ class Plot(Graph):
value, label, style = value_label_style value, label, style = value_label_style
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 = etree.SubElement(self.graph, 'path', { path = node(self.graph, 'path', {
'd': 'M 0 %(start)s h%(stop)s' % vars(), 'd': 'M 0 %s h%s' % (start, stop),
'class': 'constantLine'}) 'class': 'constantLine'})
if style: if style:
path.set('style', style) path.set('style', style)
text = etree.SubElement(self.graph, 'text', { text = node(self.graph, 'text', {
'x': str(2), 'x': 2,
'y': str(start - 2), 'y': start - 2,
'class': 'constantLine'}) 'class': 'constantLine'})
text.text = label text.text = label
@ -338,8 +253,6 @@ class Plot(Graph):
x_step = self.__transform_parameters['x_step'] x_step = self.__transform_parameters['x_step']
y_min = self.__transform_parameters['y_min'] y_min = self.__transform_parameters['y_min']
y_step = self.__transform_parameters['y_step'] y_step = self.__transform_parameters['y_step']
#locals().update(self.__transform_parameters)
#vars().update(self.__transform_parameters)
x = (x - x_min) * x_step x = (x - x_min) * x_step
y = self.graph_height - (y - y_min) * y_step y = self.graph_height - (y - y_min) * y_step
return x, y return x, y
@ -350,11 +263,11 @@ class Plot(Graph):
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:
etree.SubElement(self.graph, 'circle', { node(self.graph, 'circle', {
'cx': str(gx), 'cx': gx,
'cy': str(gy), 'cy': gy,
'r': '2.5', 'r': '2.5',
'class': 'dataPoint%(line)s' % vars()}) 'class': 'dataPoint%s' % line})
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)

135
pygal/schedule.py

@ -1,115 +1,36 @@
#!python # -*- coding: utf-8 -*-
import re 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 lxml import etree
from pygal.util import (node, grouper, date_range,
divide_timedelta_float, TimeScale)
from pygal.graph import Graph from pygal.graph import Graph
from util import grouper, date_range, divide_timedelta_float, TimeScale
__all__ = ('Schedule') __all__ = ('Schedule')
class Schedule(Graph): class Schedule(Graph):
""" """
# === For creating SVG plots of scalar temporal data Graph of temporal scalar 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 <serATgermaneHYPHENsoftwareDOTcom>
Copyright 2004 Sean E. Russell
This software is available under the Ruby license[LICENSE.txt]
""" """
"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' x_label_format = '%Y-%m-%d %H:%M:%S'
""" # Use this to set the spacing between dates on the axis. The value
Use this to set the spacing between dates on the axis. The value # must be of the form
must be of the form # "\d+ ?((year|month|week|day|hour|minute|second)s?)?"
"\d+ ?((year|month|week|day|hour|minute|second)s?)?" # e.g.
# graph.timescale_divisions = '2 weeks'
e.g. # graph.timescale_divisions = '1 month'
# graph.timescale_divisions = '3600 seconds'
graph.timescale_divisions = '2 weeks' # easier would be '1 hour'
graph.timescale_divisions = '1 month'
graph.timescale_divisions = '3600 seconds' # easier would be '1 hour'
"""
timescale_divisions = None 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' popup_format = '%Y-%m-%d %H:%M:%S'
_min_x_value = None _min_x_value = None
@ -122,23 +43,6 @@ class Schedule(Graph):
def add_data(self, data): def add_data(self, data):
""" """
Add data to the plot. 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, Note that the data must be in time,value pairs,
and that the date format and that the date format
may be any date that is parseable by ParseDate. may be any date that is parseable by ParseDate.
@ -220,7 +124,6 @@ class Schedule(Graph):
subbar_height = self.get_field_height() - bar_gap 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_min, x_max, div = self._x_range()
x_range = x_max - x_min x_range = x_max - x_min
width = (float(self.graph_width) - self.font_size * 2) width = (float(self.graph_width) - self.font_size * 2)
@ -238,11 +141,11 @@ 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)
etree.SubElement(self.graph, 'rect', { node(self.graph, 'rect', {
'x': str(bar_start), 'x': bar_start,
'y': str(y), 'y': y,
'width': str(bar_width), 'width': bar_width,
'height': str(subbar_height), 'height': subbar_height,
'class': 'fill%s' % (count + 1), 'class': 'fill%s' % (count + 1),
}) })

14
pygal/util/__init__.py

@ -1,8 +1,16 @@
#!python # -*- coding: utf-8 -*-
from lxml import etree
import itertools import itertools
import datetime 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): def grouper(n, iterable, padvalue=None):

2
test/tests.py

@ -5,4 +5,4 @@ from moulinrouge import create_app
app = create_app() app = create_app()
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=True, port=21112) app.run(debug=True, threaded=True, port=21112)

Loading…
Cancel
Save