mirror of https://github.com/Kozea/pygal.git
Florian Mounier
13 years ago
25 changed files with 0 additions and 2689 deletions
@ -1,22 +0,0 @@ |
|||||||
The MIT License |
|
||||||
|
|
||||||
Copyright © 2008 Jason R. Coombs |
|
||||||
Copyright © 2011 Kozea |
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|
||||||
of this software and associated documentation files (the "Software"), to deal |
|
||||||
in the Software without restriction, including without limitation the rights |
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
||||||
copies of the Software, and to permit persons to whom the Software is |
|
||||||
furnished to do so, subject to the following conditions: |
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in |
|
||||||
all copies or substantial portions of the Software. |
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
|
||||||
THE SOFTWARE. |
|
@ -1,7 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
|
|
||||||
""" |
|
||||||
pygal package. |
|
||||||
""" |
|
||||||
|
|
||||||
__all__ = ('graph', 'plot', 'time_series', 'bar', 'pie', 'schedule', 'util') |
|
@ -1,217 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
from itertools import chain |
|
||||||
from lxml import etree |
|
||||||
from pygal.graph import Graph |
|
||||||
from pygal.util import node |
|
||||||
|
|
||||||
__all__ = ('VerticalBar', 'HorizontalBar') |
|
||||||
|
|
||||||
|
|
||||||
class Bar(Graph): |
|
||||||
"A superclass for bar-style graphs. Do not instantiate directly." |
|
||||||
|
|
||||||
# gap between bars |
|
||||||
bar_gap = True |
|
||||||
# how to stack adjacent dataset series |
|
||||||
# overlap - overlap bars with transparent colors |
|
||||||
# top - stack bars on top of one another |
|
||||||
# side - stack bars side-by-side |
|
||||||
stack = 'side' |
|
||||||
|
|
||||||
scale_divisions = None |
|
||||||
|
|
||||||
stylesheet_names = Graph.stylesheet_names + ['bar.css'] |
|
||||||
|
|
||||||
def __init__(self, fields, *args, **kargs): |
|
||||||
self.fields = fields |
|
||||||
super(Bar, self).__init__(*args, **kargs) |
|
||||||
|
|
||||||
# adapted from Plot |
|
||||||
def get_data_values(self): |
|
||||||
min_value, max_value, scale_division = self.data_range() |
|
||||||
result = tuple( |
|
||||||
float_range(min_value, max_value + scale_division, scale_division)) |
|
||||||
if self.scale_integers: |
|
||||||
result = map(int, result) |
|
||||||
return result |
|
||||||
|
|
||||||
# adapted from plot (very much like calling data_range('y')) |
|
||||||
def data_range(self): |
|
||||||
min_value = self.data_min() |
|
||||||
max_value = self.data_max() |
|
||||||
range = max_value - min_value |
|
||||||
|
|
||||||
data_pad = range / 20.0 or 10 |
|
||||||
scale_range = (max_value + data_pad) - min_value |
|
||||||
|
|
||||||
scale_division = self.scale_divisions or (scale_range / 10.0) |
|
||||||
|
|
||||||
if self.scale_integers: |
|
||||||
scale_division = round(scale_division) or 1 |
|
||||||
|
|
||||||
return min_value, max_value, scale_division |
|
||||||
|
|
||||||
def get_field_labels(self): |
|
||||||
return self.fields |
|
||||||
|
|
||||||
def get_data_labels(self): |
|
||||||
return map(str, self.get_data_values()) |
|
||||||
|
|
||||||
def data_max(self): |
|
||||||
return max( |
|
||||||
list(chain(*map(lambda set: set['data'], self.data))) + [0]) |
|
||||||
# above is same as |
|
||||||
# return max(map(lambda set: max(set['data']), self.data)) |
|
||||||
|
|
||||||
def data_min(self): |
|
||||||
if not getattr(self, 'min_scale_value') is None: |
|
||||||
return self.min_scale_value |
|
||||||
min_value = min(list( |
|
||||||
chain(*map(lambda set: set['data'], self.data))) + [0]) |
|
||||||
return min_value |
|
||||||
|
|
||||||
def get_bar_gap(self, field_size): |
|
||||||
bar_gap = 10 # default gap |
|
||||||
if field_size < 10: |
|
||||||
# adjust for narrow fields |
|
||||||
bar_gap = field_size / 2 |
|
||||||
# the following zero's out the gap if bar_gap is False |
|
||||||
bar_gap = int(self.bar_gap) * bar_gap |
|
||||||
return bar_gap |
|
||||||
|
|
||||||
|
|
||||||
def float_range(start=0, stop=None, step=1): |
|
||||||
"Much like the built-in function range, but accepts floats" |
|
||||||
while start < stop: |
|
||||||
yield float(start) |
|
||||||
start += step |
|
||||||
|
|
||||||
|
|
||||||
class VerticalBar(Bar): |
|
||||||
""" Vertical bar graph """ |
|
||||||
top_align = top_font = 1 |
|
||||||
|
|
||||||
def get_x_labels(self): |
|
||||||
return self.get_field_labels() |
|
||||||
|
|
||||||
def get_y_labels(self): |
|
||||||
return self.get_data_labels() |
|
||||||
|
|
||||||
def x_label_offset(self, width): |
|
||||||
return width / 2.0 |
|
||||||
|
|
||||||
def draw_data(self): |
|
||||||
min_value = self.data_min() |
|
||||||
unit_size = ( |
|
||||||
float(self.graph_height) - self.font_size * 2 * self.top_font) |
|
||||||
divisor = ( |
|
||||||
max(self.get_data_values()) - min(self.get_data_values())) |
|
||||||
if divisor == 0: |
|
||||||
unit_size = 0 |
|
||||||
else: |
|
||||||
unit_size /= divisor |
|
||||||
|
|
||||||
bar_gap = self.get_bar_gap(self.get_field_width()) |
|
||||||
|
|
||||||
bar_width = self.get_field_width() - bar_gap |
|
||||||
if self.stack == 'side': |
|
||||||
bar_width /= len(self.data) |
|
||||||
|
|
||||||
x_mod = (self.graph_width - bar_gap) / 2 |
|
||||||
if self.stack == 'side': |
|
||||||
x_mod -= bar_width / 2 |
|
||||||
|
|
||||||
bottom = self.graph_height |
|
||||||
|
|
||||||
for field_count, field in enumerate(self.fields): |
|
||||||
for dataset_count, dataset in enumerate(self.data): |
|
||||||
# cases (assume 0 = +ve): |
|
||||||
# value min length |
|
||||||
# +ve +ve value - min |
|
||||||
# +ve -ve value - 0 |
|
||||||
# -ve -ve value.abs - 0 |
|
||||||
value = dataset['data'][field_count] |
|
||||||
|
|
||||||
left = self.get_field_width() * field_count |
|
||||||
|
|
||||||
length = (abs(value) - max(min_value, 0)) * unit_size |
|
||||||
# top is 0 if value is negative |
|
||||||
top = bottom - ((max(value, 0) - min_value) * unit_size) |
|
||||||
if self.stack == 'side': |
|
||||||
left += bar_width * dataset_count |
|
||||||
|
|
||||||
rect_group = node(self.graph, "g", |
|
||||||
{'class': 'bar vbar'}) |
|
||||||
node(rect_group, 'rect', { |
|
||||||
'x': left, |
|
||||||
'y': top, |
|
||||||
'width': bar_width, |
|
||||||
'height': length, |
|
||||||
'class': 'fill fill%s' % (dataset_count + 1), |
|
||||||
}) |
|
||||||
|
|
||||||
self.make_datapoint_text( |
|
||||||
rect_group, left + bar_width / 2.0, top - 6, value) |
|
||||||
|
|
||||||
|
|
||||||
class HorizontalBar(Bar): |
|
||||||
""" Horizontal bar graph """ |
|
||||||
rotate_y_labels = True |
|
||||||
show_x_guidelines = True |
|
||||||
show_y_guidelines = False |
|
||||||
right_align = right_font = True |
|
||||||
|
|
||||||
def get_x_labels(self): |
|
||||||
return self.get_data_labels() |
|
||||||
|
|
||||||
def get_y_labels(self): |
|
||||||
return self.get_field_labels() |
|
||||||
|
|
||||||
def y_label_offset(self, height): |
|
||||||
return height / -2.0 |
|
||||||
|
|
||||||
def draw_data(self): |
|
||||||
min_value = self.data_min() |
|
||||||
|
|
||||||
unit_size = float(self.graph_width) |
|
||||||
unit_size -= self.font_size * 2 * self.right_font |
|
||||||
unit_size /= max(self.get_data_values()) - min(self.get_data_values()) |
|
||||||
|
|
||||||
bar_gap = self.get_bar_gap(self.get_field_height()) |
|
||||||
|
|
||||||
bar_height = self.get_field_height() - bar_gap |
|
||||||
if self.stack == 'side': |
|
||||||
bar_height /= len(self.data) |
|
||||||
|
|
||||||
y_mod = (bar_height / 2) + (self.font_size / 2) |
|
||||||
|
|
||||||
for field_count, field in enumerate(self.fields): |
|
||||||
for dataset_count, dataset in enumerate(self.data): |
|
||||||
value = dataset['data'][field_count] |
|
||||||
|
|
||||||
top = self.graph_height - ( |
|
||||||
self.get_field_height() * (field_count + 1)) |
|
||||||
if self.stack == 'side': |
|
||||||
top += (bar_height * dataset_count) |
|
||||||
# cases (assume 0 = +ve): |
|
||||||
# value min length left |
|
||||||
# +ve +ve value.abs - min minvalue.abs |
|
||||||
# +ve -ve value.abs - 0 minvalue.abs |
|
||||||
# -ve -ve value.abs - 0 minvalue.abs + value |
|
||||||
length = (abs(value) - max(min_value, 0)) * unit_size |
|
||||||
# left is 0 if value is negative |
|
||||||
left = (abs(min_value) + min(value, 0)) * unit_size |
|
||||||
|
|
||||||
rect_group = node(self.graph, "g", |
|
||||||
{'class': 'bar hbar'}) |
|
||||||
node(rect_group, 'rect', { |
|
||||||
'x': left, |
|
||||||
'y': top, |
|
||||||
'width': length, |
|
||||||
'height': bar_height, |
|
||||||
'class': 'fill fill%s' % (dataset_count + 1), |
|
||||||
}) |
|
||||||
|
|
||||||
self.make_datapoint_text(rect_group, |
|
||||||
left + length + 5, top + y_mod, value, |
|
||||||
"text-anchor: start; ") |
|
@ -1,127 +0,0 @@ |
|||||||
/* Base styles for pygal graphs */ |
|
||||||
|
|
||||||
* { |
|
||||||
font-family: helvetica; |
|
||||||
} |
|
||||||
.svgBackground{ |
|
||||||
fill: #fff; |
|
||||||
} |
|
||||||
.graphBackground{ |
|
||||||
fill: #e8eef6; |
|
||||||
fill-opacity: 0.7; |
|
||||||
} |
|
||||||
|
|
||||||
/* graphs titles */ |
|
||||||
.mainTitle{ |
|
||||||
text-anchor: middle; |
|
||||||
fill: #000000; |
|
||||||
font-size: %(title_font_size)dpx; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
.subTitle{ |
|
||||||
text-anchor: middle; |
|
||||||
fill: #999999; |
|
||||||
font-size: %(subtitle_font_size)dpx; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
|
|
||||||
.axis{ |
|
||||||
stroke: #000000; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
|
|
||||||
.guideLines{ |
|
||||||
stroke: #fff; |
|
||||||
stroke-width: 1px; |
|
||||||
stroke-dasharray: 5,5; |
|
||||||
} |
|
||||||
|
|
||||||
.xAxisLabels{ |
|
||||||
font-family: monospace; |
|
||||||
text-anchor: middle; |
|
||||||
fill: #000000; |
|
||||||
font-size: %(x_label_font_size)dpx; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
|
|
||||||
.yAxisLabels{ |
|
||||||
font-family: monospace; |
|
||||||
text-anchor: end; |
|
||||||
fill: #000000; |
|
||||||
font-size: %(y_label_font_size)dpx; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
|
|
||||||
.xAxisTitle{ |
|
||||||
text-anchor: middle; |
|
||||||
fill: #ff0000; |
|
||||||
font-size: %(x_title_font_size)dpx; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
|
|
||||||
.yAxisTitle{ |
|
||||||
fill: #ff0000; |
|
||||||
text-anchor: middle; |
|
||||||
font-size: %(y_title_font_size)dpx; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
|
|
||||||
.dataPointLabel{ |
|
||||||
text-anchor:middle; |
|
||||||
font-size: 10px; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
|
|
||||||
.staggerGuideLine{ |
|
||||||
fill: none; |
|
||||||
stroke: #000000; |
|
||||||
stroke-width: 0.5px; |
|
||||||
} |
|
||||||
|
|
||||||
.keyText{ |
|
||||||
fill: #000000; |
|
||||||
text-anchor:start; |
|
||||||
font-size: %(key_font_size)dpx; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
.bar .dataPointLabel, .dot .dataPointLabel { |
|
||||||
fill-opacity: 0; |
|
||||||
-webkit-transition: 250ms; |
|
||||||
} |
|
||||||
|
|
||||||
.bar:hover .dataPointLabel, .dot:hover .dataPointLabel { |
|
||||||
fill-opacity: 0.9; |
|
||||||
fill: #000000; |
|
||||||
} |
|
||||||
|
|
||||||
.upGradientLight { |
|
||||||
stop-opacity: 0.6; |
|
||||||
} |
|
||||||
|
|
||||||
.downGradientLight { |
|
||||||
stop-opacity: 0.9; |
|
||||||
} |
|
||||||
|
|
||||||
.key, .fill { |
|
||||||
fill-opacity: 0.9; |
|
||||||
stroke: #fff; |
|
||||||
stroke-width: 2px; |
|
||||||
-webkit-transition: 250ms; |
|
||||||
} |
|
||||||
|
|
||||||
.fill:hover { |
|
||||||
stroke: #ddd; |
|
||||||
fill-opacity: 0.7; |
|
||||||
} |
|
||||||
|
|
||||||
.line { |
|
||||||
fill: none; |
|
||||||
stroke-width: 1.5px; |
|
||||||
-webkit-transition: 250ms; |
|
||||||
} |
|
||||||
|
|
||||||
.line:hover { |
|
||||||
stroke-width: 3px; |
|
||||||
} |
|
@ -1,11 +0,0 @@ |
|||||||
.dataPointLabel{ |
|
||||||
fill: #000000; |
|
||||||
text-anchor:middle; |
|
||||||
font-size: %(datapoint_font_size)spx; |
|
||||||
fill-opacity: 0; |
|
||||||
} |
|
||||||
|
|
||||||
.pie:hover .dataPointLabel { |
|
||||||
fill-opacity: 0.9; |
|
||||||
fill: #000000; |
|
||||||
} |
|
@ -1,600 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
|
|
||||||
""" |
|
||||||
pygal.graph |
|
||||||
|
|
||||||
The base module for `pygal` classes. |
|
||||||
""" |
|
||||||
|
|
||||||
from operator import itemgetter |
|
||||||
from itertools import islice |
|
||||||
from logging import getLogger |
|
||||||
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) |
|
||||||
|
|
||||||
log = getLogger('pygal') |
|
||||||
|
|
||||||
|
|
||||||
def sort_multiple(arrays): |
|
||||||
"sort multiple lists (of equal size) " |
|
||||||
"using the first list for the sort keys" |
|
||||||
tuples = zip(*arrays) |
|
||||||
tuples.sort() |
|
||||||
return zip(*tuples) |
|
||||||
|
|
||||||
|
|
||||||
class Graph(object): |
|
||||||
""" |
|
||||||
Base object for generating SVG Graphs |
|
||||||
|
|
||||||
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. |
|
||||||
|
|
||||||
For examples of how to subclass this class, see the existing specific |
|
||||||
subclasses, such as svn.charts.Pie. |
|
||||||
|
|
||||||
* pygal.bar |
|
||||||
* pygal.line |
|
||||||
* pygal.pie |
|
||||||
* pygal.plot |
|
||||||
* pygal.time_series |
|
||||||
|
|
||||||
""" |
|
||||||
ratio = .7 |
|
||||||
width = 600 |
|
||||||
show_x_guidelines = False |
|
||||||
show_y_guidelines = True |
|
||||||
show_data_values = True |
|
||||||
min_scale_value = None |
|
||||||
show_x_labels = True |
|
||||||
stagger_x_labels = False |
|
||||||
x_label_rotation = 0 |
|
||||||
step_x_labels = 1 |
|
||||||
step_include_first_x_label = True |
|
||||||
show_y_labels = True |
|
||||||
rotate_y_labels = False |
|
||||||
stagger_y_labels = False |
|
||||||
step_include_first_y_label = True |
|
||||||
step_y_labels = 1 |
|
||||||
scale_integers = False |
|
||||||
show_x_title = False |
|
||||||
x_title = 'X Field names' |
|
||||||
show_y_title = False |
|
||||||
# 'bt' for bottom to top; 'tb' for top to bottom |
|
||||||
y_title_text_direction = 'bt' |
|
||||||
y_title = 'Y Scale' |
|
||||||
show_graph_title = False |
|
||||||
graph_title = 'Graph Title' |
|
||||||
show_graph_subtitle = False |
|
||||||
graph_subtitle = 'Graph Subtitle' |
|
||||||
key = True |
|
||||||
# 'bottom' or 'right', |
|
||||||
key_position = 'right' |
|
||||||
|
|
||||||
font_size = 12 |
|
||||||
title_font_size = 16 |
|
||||||
subtitle_font_size = 14 |
|
||||||
x_label_font_size = 12 |
|
||||||
x_title_font_size = 14 |
|
||||||
y_label_font_size = 12 |
|
||||||
y_title_font_size = 14 |
|
||||||
key_font_size = 10 |
|
||||||
key_box_size = 10 |
|
||||||
add_popups = False |
|
||||||
|
|
||||||
top_align = top_font = right_align = right_font = 0 |
|
||||||
stylesheet_names = ['graph.css'] |
|
||||||
compress = False |
|
||||||
colors = [ |
|
||||||
"#2a4269", "#476fb2", "#38588e", "#698bc3", |
|
||||||
"#69c38b", "#588e38", "#47b26f", "#42692a", |
|
||||||
"#1a3259", "#375fa2", "#28487e", "#597bb3", |
|
||||||
"#59b37b", "#487e28", "#37a25f", "#32591a"] |
|
||||||
|
|
||||||
def __init__(self, config={}): |
|
||||||
"""Initialize the graph object with the graph settings.""" |
|
||||||
if self.__class__ is Graph: |
|
||||||
raise NotImplementedError("Graph is an abstract base class") |
|
||||||
self.load_config(config) |
|
||||||
self.clear_data() |
|
||||||
|
|
||||||
@property |
|
||||||
def height(self): |
|
||||||
return int(self.width * self.ratio) |
|
||||||
|
|
||||||
def load_config(self, config): |
|
||||||
self.__dict__.update(config) |
|
||||||
|
|
||||||
def add_data(self, conf): |
|
||||||
""" |
|
||||||
Add data to the graph object. May be called several times to add |
|
||||||
additional data sets. |
|
||||||
|
|
||||||
>>> data_sales_02 = [12, 45, 21] # doctest: +SKIP |
|
||||||
>>> graph.add_data({ # doctest: +SKIP |
|
||||||
... 'data': data_sales_02, |
|
||||||
... 'title': 'Sales 2002' |
|
||||||
... }) # doctest: +SKIP |
|
||||||
""" |
|
||||||
self.validate_data(conf) |
|
||||||
self.process_data(conf) |
|
||||||
self.data.append(conf) |
|
||||||
|
|
||||||
def validate_data(self, conf): |
|
||||||
try: |
|
||||||
assert(isinstance(conf['data'], (tuple, list))) |
|
||||||
except TypeError: |
|
||||||
raise TypeError( |
|
||||||
"conf should be dictionary with 'data' and other items") |
|
||||||
except AssertionError: |
|
||||||
if not hasattr(conf['data'], '__iter__'): |
|
||||||
raise TypeError( |
|
||||||
"conf['data'] should be tuple or list or iterable") |
|
||||||
|
|
||||||
def process_data(self, data): |
|
||||||
pass |
|
||||||
|
|
||||||
def clear_data(self): |
|
||||||
""" |
|
||||||
This method removes all data from the object so that you can |
|
||||||
reuse it to create a new graph but with the same config options. |
|
||||||
|
|
||||||
>>> graph.clear_data() # doctest: +SKIP |
|
||||||
""" |
|
||||||
self.data = [] |
|
||||||
|
|
||||||
def burn(self): |
|
||||||
""" |
|
||||||
Process the template with the data and |
|
||||||
config which has been set and return the resulting SVG. |
|
||||||
|
|
||||||
Raises ValueError when no data set has |
|
||||||
been added to the graph object. |
|
||||||
""" |
|
||||||
|
|
||||||
log.info("Burning %s graph" % self.__class__.__name__) |
|
||||||
|
|
||||||
if not self.data: |
|
||||||
raise ValueError("No data available") |
|
||||||
|
|
||||||
if hasattr(self, 'calculations'): |
|
||||||
self.calculations() |
|
||||||
|
|
||||||
self.start_svg() |
|
||||||
self.calculate_graph_dimensions() |
|
||||||
self.foreground = etree.Element("g") |
|
||||||
|
|
||||||
self.draw_graph() |
|
||||||
self.draw_titles() |
|
||||||
self.draw_legend() |
|
||||||
self.draw_data() |
|
||||||
|
|
||||||
self.graph.append(self.foreground) |
|
||||||
|
|
||||||
data = etree.tostring( |
|
||||||
self.root, pretty_print=True, |
|
||||||
xml_declaration=True, encoding='utf-8') |
|
||||||
if self.compress: |
|
||||||
import zlib |
|
||||||
data = zlib.compress(data) |
|
||||||
|
|
||||||
return data |
|
||||||
|
|
||||||
def max_y_label_width_px(self): |
|
||||||
""" |
|
||||||
Calculate the width of the widest Y label. This will be the |
|
||||||
character height if the Y labels are rotated. |
|
||||||
""" |
|
||||||
if self.rotate_y_labels: |
|
||||||
return self.font_size |
|
||||||
|
|
||||||
def draw_graph(self): |
|
||||||
""" |
|
||||||
The central logic for drawing the graph. |
|
||||||
|
|
||||||
Sets self.graph (the 'g' element in the SVG root) |
|
||||||
""" |
|
||||||
transform = 'translate (%s %s)' % (self.border_left, self.border_top) |
|
||||||
self.graph = node(self.root, 'g', transform=transform) |
|
||||||
self.back = node(self.graph, 'g', {'class': 'back'}) |
|
||||||
axis = node(self.foreground, 'g', {'class': 'axis'}) |
|
||||||
node(self.back, 'rect', { |
|
||||||
'x': 0, |
|
||||||
'y': 0, |
|
||||||
'width': self.graph_width, |
|
||||||
'height': self.graph_height, |
|
||||||
'class': 'graphBackground' |
|
||||||
}) |
|
||||||
|
|
||||||
#Axis |
|
||||||
node(axis, 'path', { |
|
||||||
'd': 'M 0 0 v%s' % self.graph_height, |
|
||||||
'class': 'axis', |
|
||||||
'id': 'xAxis' |
|
||||||
}) |
|
||||||
node(axis, '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() |
|
||||||
|
|
||||||
def make_datapoint_text(self, group, x, y, value, style=None): |
|
||||||
""" |
|
||||||
Add text for a datapoint |
|
||||||
""" |
|
||||||
if not self.show_data_values: |
|
||||||
return |
|
||||||
|
|
||||||
e = node(group, 'text', { |
|
||||||
'x': x, |
|
||||||
'y': y, |
|
||||||
'class': 'dataPointLabel'}) |
|
||||||
e.text = str(value) |
|
||||||
if style: |
|
||||||
e.set('style', style) |
|
||||||
|
|
||||||
def x_label_offset(self, width): |
|
||||||
return 0 |
|
||||||
|
|
||||||
def draw_x_labels(self): |
|
||||||
"Draw the X axis labels" |
|
||||||
if not self.show_x_labels: |
|
||||||
return |
|
||||||
|
|
||||||
log.debug("Drawing x labels") |
|
||||||
self.xlabels = node(self.graph, 'g', {'class': 'xLabels'}) |
|
||||||
labels = self.get_x_labels() |
|
||||||
count = len(labels) |
|
||||||
labels = enumerate(iter(labels)) |
|
||||||
start = int(not self.step_include_first_x_label) |
|
||||||
labels = islice(labels, start, None, self.step_x_labels) |
|
||||||
map(self.draw_x_label, labels) |
|
||||||
self.draw_x_guidelines(self.field_width(), count) |
|
||||||
|
|
||||||
def draw_x_label(self, label): |
|
||||||
label_width = self.field_width() |
|
||||||
index, label = label |
|
||||||
text = node(self.xlabels, '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 |
|
||||||
|
|
||||||
if self.stagger_x_labels and (index % 2): |
|
||||||
stagger = self.x_label_font_size + 5 |
|
||||||
y += stagger |
|
||||||
graph_height = self.graph_height |
|
||||||
node(self.xlabels, 'path', { |
|
||||||
'd': 'M%f %f v%d' % (x, graph_height, stagger), |
|
||||||
'class': 'staggerGuideLine' |
|
||||||
}) |
|
||||||
|
|
||||||
text.set('x', str(x)) |
|
||||||
text.set('y', str(y)) |
|
||||||
|
|
||||||
if self.x_label_rotation: |
|
||||||
transform = 'rotate(%d %d %d) translate(0 -%d)' % \ |
|
||||||
(-self.x_label_rotation, x, y - self.x_label_font_size, |
|
||||||
self.x_label_font_size / 4) |
|
||||||
text.set('transform', transform) |
|
||||||
text.set('style', 'text-anchor: end') |
|
||||||
else: |
|
||||||
text.set('style', 'text-anchor: middle') |
|
||||||
|
|
||||||
def y_label_offset(self, height): |
|
||||||
""" |
|
||||||
Return an offset for drawing the y label. Currently returns 0. |
|
||||||
""" |
|
||||||
# Consider height/2 to center within the field. |
|
||||||
return 0 |
|
||||||
|
|
||||||
def get_field_width(self): |
|
||||||
divisor = (len(self.get_x_labels()) - self.right_align) |
|
||||||
if divisor == 0: |
|
||||||
return 0 |
|
||||||
return float( |
|
||||||
self.graph_width - self.font_size * 2 * self.right_font) / divisor |
|
||||||
field_width = get_field_width |
|
||||||
|
|
||||||
def get_field_height(self): |
|
||||||
divisor = (len(self.get_y_labels()) - self.top_align) |
|
||||||
if divisor == 0: |
|
||||||
return 0 |
|
||||||
return float( |
|
||||||
self.graph_height - self.font_size * 2 * self.top_font) / divisor |
|
||||||
field_height = get_field_height |
|
||||||
|
|
||||||
def draw_y_labels(self): |
|
||||||
"Draw the Y axis labels" |
|
||||||
if not self.show_y_labels: |
|
||||||
return |
|
||||||
log.debug("Drawing y labels") |
|
||||||
|
|
||||||
self.ylabels = node(self.graph, 'g', {'class': 'yLabels'}) |
|
||||||
labels = self.get_y_labels() |
|
||||||
count = len(labels) |
|
||||||
|
|
||||||
labels = enumerate(iter(labels)) |
|
||||||
start = int(not self.step_include_first_y_label) |
|
||||||
labels = islice(labels, start, None, self.step_y_labels) |
|
||||||
map(self.draw_y_label, labels) |
|
||||||
self.draw_y_guidelines(self.field_height(), count) |
|
||||||
|
|
||||||
def get_y_offset(self): |
|
||||||
result = self.graph_height + self.y_label_offset(self.field_height()) |
|
||||||
if not self.rotate_y_labels: |
|
||||||
result += self.font_size / 1.2 |
|
||||||
return result |
|
||||||
y_offset = property(get_y_offset) |
|
||||||
|
|
||||||
def draw_y_label(self, label): |
|
||||||
label_height = self.field_height() |
|
||||||
index, label = label |
|
||||||
text = node(self.ylabels, 'text', {'class': 'yAxisLabels'}) |
|
||||||
text.text = label |
|
||||||
|
|
||||||
y = self.y_offset - (label_height * index) |
|
||||||
x = {True: 0, False: -3}[self.rotate_y_labels] |
|
||||||
|
|
||||||
if self.stagger_y_labels and (index % 2): |
|
||||||
stagger = self.y_label_font_size + 5 |
|
||||||
x -= stagger |
|
||||||
node(self.ylabels, 'path', { |
|
||||||
'd': 'M%f %f h%d' % (x, y, stagger), |
|
||||||
'class': 'staggerGuideLine' |
|
||||||
}) |
|
||||||
|
|
||||||
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.set('transform', transform) |
|
||||||
text.set('style', 'text-anchor: middle') |
|
||||||
else: |
|
||||||
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" |
|
||||||
if not self.show_x_guidelines: |
|
||||||
return |
|
||||||
log.debug("Drawing x guidelines") |
|
||||||
self.xguidelines = node(self.graph, 'g', {'class': 'xGuideLines'}) |
|
||||||
# skip the first one |
|
||||||
for count in range(1, count): |
|
||||||
start = label_height * count |
|
||||||
stop = self.graph_height |
|
||||||
node(self.xguidelines, 'path', { |
|
||||||
'd': 'M %s 0 v%s' % (start, stop), |
|
||||||
'class': 'guideLines'}) |
|
||||||
|
|
||||||
def draw_y_guidelines(self, label_height, count): |
|
||||||
"Draw the Y-axis guidelines" |
|
||||||
if not self.show_y_guidelines: |
|
||||||
return |
|
||||||
log.debug("Drawing y guidelines") |
|
||||||
self.yguidelines = node(self.graph, 'g', {'class': 'yGuideLines'}) |
|
||||||
for count in range(1, count): |
|
||||||
start = self.graph_height - label_height * count |
|
||||||
stop = self.graph_width |
|
||||||
node(self.yguidelines, 'path', { |
|
||||||
'd': 'M 0 %s h%s' % (start, stop), |
|
||||||
'class': 'guideLines'}) |
|
||||||
|
|
||||||
def draw_titles(self): |
|
||||||
"Draws the graph title and subtitle" |
|
||||||
log.debug("Drawing titles") |
|
||||||
if self.show_graph_title: |
|
||||||
self.draw_graph_title() |
|
||||||
if self.show_graph_subtitle: |
|
||||||
self.draw_graph_subtitle() |
|
||||||
if self.show_x_title: |
|
||||||
self.draw_x_title() |
|
||||||
if self.show_y_title: |
|
||||||
self.draw_y_title() |
|
||||||
|
|
||||||
def draw_graph_title(self): |
|
||||||
text = node(self.root, 'text', { |
|
||||||
'x': self.width / 2, |
|
||||||
'y': self.title_font_size, |
|
||||||
'class': 'mainTitle'}) |
|
||||||
text.text = self.graph_title |
|
||||||
|
|
||||||
def draw_graph_subtitle(self): |
|
||||||
y_subtitle_options = [self.subtitle_font_size, |
|
||||||
self.title_font_size + 10] |
|
||||||
y_subtitle = y_subtitle_options[self.show_graph_title] |
|
||||||
text = node(self.root, 'text', { |
|
||||||
'x': self.width / 2, |
|
||||||
'y': y_subtitle, |
|
||||||
'class': 'subTitle', |
|
||||||
}) |
|
||||||
text.text = self.graph_title |
|
||||||
|
|
||||||
def draw_x_title(self): |
|
||||||
log.debug("Drawing x title") |
|
||||||
y = self.graph_height + self.border_top + self.x_title_font_size |
|
||||||
if self.show_x_labels: |
|
||||||
y_size = self.x_label_font_size + 5 |
|
||||||
if self.stagger_x_labels: |
|
||||||
y_size *= 2 |
|
||||||
y += y_size |
|
||||||
x = self.width / 2 |
|
||||||
|
|
||||||
text = node(self.root, 'text', { |
|
||||||
'x': x, |
|
||||||
'y': y, |
|
||||||
'class': 'xAxisTitle', |
|
||||||
}) |
|
||||||
text.text = self.x_title |
|
||||||
|
|
||||||
def draw_y_title(self): |
|
||||||
log.debug("Drawing y title") |
|
||||||
x = self.y_title_font_size |
|
||||||
if self.y_title_text_direction == 'bt': |
|
||||||
x += 3 |
|
||||||
rotate = -90 |
|
||||||
else: |
|
||||||
x -= 3 |
|
||||||
rotate = 90 |
|
||||||
y = self.height / 2 |
|
||||||
text = node(self.root, 'text', { |
|
||||||
'x': x, |
|
||||||
'y': y, |
|
||||||
'class': 'yAxisTitle', |
|
||||||
}) |
|
||||||
text.text = self.y_title |
|
||||||
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: |
|
||||||
return |
|
||||||
log.debug("Drawing legend") |
|
||||||
|
|
||||||
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) |
|
||||||
node(group, 'rect', { |
|
||||||
'x': 0, |
|
||||||
'y': y_offset, |
|
||||||
'width': self.key_box_size, |
|
||||||
'height': self.key_box_size, |
|
||||||
'class': 'key key%s' % key_count, |
|
||||||
}) |
|
||||||
text = node(group, 'text', { |
|
||||||
'x': self.key_box_size + 5, |
|
||||||
'y': y_offset + self.key_box_size, |
|
||||||
'class': 'keyText'}) |
|
||||||
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 = calculate_offsets_bottom(self) |
|
||||||
group.set('transform', 'translate(%d %d)' % (x_offset, y_offset)) |
|
||||||
|
|
||||||
def add_defs(self, defs): |
|
||||||
""" |
|
||||||
Override and place code to add defs here. TODO: what are defs? |
|
||||||
""" |
|
||||||
for id in range(len(self.colors)): |
|
||||||
idn = 'line-color-%d' % id |
|
||||||
light = node(defs, 'linearGradient', { |
|
||||||
'id': idn, |
|
||||||
'x1': 0, |
|
||||||
'x2': '50%', |
|
||||||
'y1': 0, |
|
||||||
'y2': '100%'}) |
|
||||||
node(light, 'stop', |
|
||||||
{'class': 'upGradientLight %s' % idn, 'offset': 0}) |
|
||||||
node(light, 'stop', |
|
||||||
{'class': 'downGradientLight %s' % idn, 'offset': '100%'}) |
|
||||||
|
|
||||||
shadow = node(defs, 'linearGradient', { |
|
||||||
'id': 'shadow', |
|
||||||
'x1': 0, |
|
||||||
'x2': '100%', |
|
||||||
'y1': 0, |
|
||||||
'y2': 0}) |
|
||||||
node(shadow, 'stop', |
|
||||||
{'offset': 0, 'stop-color': '#aaa', 'stop-opacity': 0.7}) |
|
||||||
node(shadow, 'stop', |
|
||||||
{'offset': '1%', 'stop-color': '#fff', 'stop-opacity': 1}) |
|
||||||
node(shadow, 'stop', |
|
||||||
{'offset': '99%', 'stop-color': '#fff', 'stop-opacity': 1}) |
|
||||||
node(shadow, 'stop', |
|
||||||
{'offset': '100%', 'stop-color': '#aaa', 'stop-opacity': .7}) |
|
||||||
|
|
||||||
def start_svg(self): |
|
||||||
"Base SVG Document Creation" |
|
||||||
log.debug("Creating root node") |
|
||||||
svg_ns = 'http://www.w3.org/2000/svg' |
|
||||||
nsmap = { |
|
||||||
None: svg_ns, |
|
||||||
'xlink': 'http://www.w3.org/1999/xlink', |
|
||||||
} |
|
||||||
self.root = etree.Element("{%s}svg" % svg_ns, attrib={ |
|
||||||
'viewBox': '0 0 %d %d' % (self.width, self.height) |
|
||||||
}, 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 = ( |
|
||||||
u'Generated with pygal ©Kozea 2011', |
|
||||||
'Based upon SVG.Graph by Jason R. Coombs', |
|
||||||
) |
|
||||||
map(self.root.append, map(etree.Comment, comment_strings)) |
|
||||||
|
|
||||||
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 = node(defs, 'style', type='text/css') |
|
||||||
style.text = '' |
|
||||||
opts = self.__dict__.copy() |
|
||||||
opts.update(Graph.__dict__) |
|
||||||
opts.update(self.__class__.__dict__) |
|
||||||
for stylesheet in self.stylesheet_names: |
|
||||||
with open( |
|
||||||
os.path.join(os.path.dirname(__file__), 'css', |
|
||||||
stylesheet)) as f: |
|
||||||
style.text += f.read() % opts |
|
||||||
for n, color in enumerate(self.colors): |
|
||||||
style.text += ( |
|
||||||
""" |
|
||||||
.key%d, .fill%d, .dot%d { |
|
||||||
fill: url(#line-color-%d); |
|
||||||
} |
|
||||||
.key%d, .line%d { |
|
||||||
stroke: url(#line-color-%d); |
|
||||||
} |
|
||||||
|
|
||||||
.line-color-%d { |
|
||||||
stop-color: %s; |
|
||||||
} |
|
||||||
|
|
||||||
""" % (n, n, n, n, n, n, n, n, color)) |
|
||||||
|
|
||||||
if hasattr(self, 'stylesheet_file'): |
|
||||||
with open(self.stylesheet_file) as f: |
|
||||||
style.text += f.read() % opts |
|
||||||
|
|
||||||
self.root.append(etree.Comment('SVG Background')) |
|
||||||
node(self.root, 'rect', { |
|
||||||
'width': self.width, |
|
||||||
'height': self.height, |
|
||||||
'x': 0, |
|
||||||
'y': 0, |
|
||||||
'class': 'svgBackground'}) |
|
||||||
|
|
||||||
def calculate_graph_dimensions(self): |
|
||||||
log.debug("Computing sizes") |
|
||||||
self.border_right = calculate_right_margin(self) |
|
||||||
self.border_top = calculate_top_margin(self) |
|
||||||
self.border_left = calculate_left_margin(self) |
|
||||||
self.border_bottom = calculate_bottom_margin(self) |
|
||||||
|
|
||||||
self.graph_width = self.width - self.border_left - self.border_right |
|
||||||
self.graph_height = self.height - self.border_top - self.border_bottom |
|
@ -1,172 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
from operator import itemgetter, add |
|
||||||
|
|
||||||
from pygal.util import node, flatten, float_range |
|
||||||
from pygal.graph import Graph |
|
||||||
|
|
||||||
|
|
||||||
class Line(Graph): |
|
||||||
"""Line Graph""" |
|
||||||
|
|
||||||
"""Show a small circle on the graph where the line goes from one point to |
|
||||||
the next""" |
|
||||||
show_data_points = True |
|
||||||
show_data_values = True |
|
||||||
"""Accumulates each data set. (i.e. Each point increased by sum of all |
|
||||||
previous series at same point).""" |
|
||||||
stacked = False |
|
||||||
"Fill in the area under the plot" |
|
||||||
area_fill = False |
|
||||||
|
|
||||||
scale_divisions = None |
|
||||||
|
|
||||||
# override some defaults |
|
||||||
top_align = top_font = right_align = right_font = True |
|
||||||
|
|
||||||
stylesheet_names = Graph.stylesheet_names + ['plot.css'] |
|
||||||
|
|
||||||
def max_value(self): |
|
||||||
data = map(itemgetter('data'), self.data) |
|
||||||
if self.stacked: |
|
||||||
data = self.get_cumulative_data() |
|
||||||
return max(flatten(data)) |
|
||||||
|
|
||||||
def min_value(self): |
|
||||||
if self.min_scale_value: |
|
||||||
return self.min_scale_value |
|
||||||
data = map(itemgetter('data'), self.data) |
|
||||||
if self.stacked: |
|
||||||
data = self.get_cumulative_data() |
|
||||||
return min(flatten(data)) |
|
||||||
|
|
||||||
def get_cumulative_data(self): |
|
||||||
"""Get the data as it will be charted. The first set will be |
|
||||||
the actual first data set. The second will be the sum of the |
|
||||||
first and the second, etc.""" |
|
||||||
sets = map(itemgetter('data'), self.data) |
|
||||||
if not sets: |
|
||||||
return |
|
||||||
sum = sets.pop(0) |
|
||||||
yield sum |
|
||||||
while sets: |
|
||||||
sum = map(add, sets.pop(0)) |
|
||||||
yield sum |
|
||||||
|
|
||||||
def get_x_labels(self): |
|
||||||
return self.fields |
|
||||||
|
|
||||||
def calculate_left_margin(self): |
|
||||||
super(self.__class__, self).calculate_left_margin() |
|
||||||
label_left = len(self.fields[0]) / 2 * self.font_size * 0.6 |
|
||||||
self.border_left = max(label_left, self.border_left) |
|
||||||
|
|
||||||
# TODO: cache this |
|
||||||
def get_y_label_values(self): |
|
||||||
max_value = self.max_value() |
|
||||||
min_value = self.min_value() |
|
||||||
range = max_value - min_value |
|
||||||
|
|
||||||
scale_division = self.scale_divisions or (.105 * range) |
|
||||||
|
|
||||||
if self.scale_integers: |
|
||||||
scale_division = round(scale_division) |
|
||||||
|
|
||||||
scale_division = scale_division or 1 # prevent / 0 |
|
||||||
|
|
||||||
if max_value % scale_division == 0: |
|
||||||
max_value += scale_division |
|
||||||
labels = tuple(float_range(min_value, max_value, scale_division)) |
|
||||||
if self.scale_integers: |
|
||||||
labels = map(int, labels) |
|
||||||
return labels |
|
||||||
|
|
||||||
def get_y_labels(self): |
|
||||||
return map(str, self.get_y_label_values()) |
|
||||||
|
|
||||||
def calc_coords(self, field, value, width=None, height=None): |
|
||||||
if width is None: |
|
||||||
width = self.field_width |
|
||||||
if height is None: |
|
||||||
height = self.field_height |
|
||||||
coords = dict( |
|
||||||
x=width * field, |
|
||||||
y=self.graph_height - value * height, |
|
||||||
) |
|
||||||
return coords |
|
||||||
|
|
||||||
def draw_data(self): |
|
||||||
if len(self.data) == 0: |
|
||||||
return |
|
||||||
|
|
||||||
min_value = self.min_value() |
|
||||||
|
|
||||||
field_height = self.graph_height - self.font_size * 2 * self.top_font |
|
||||||
y_label_values = self.get_y_label_values() |
|
||||||
y_label_span = max(y_label_values) - min(y_label_values) |
|
||||||
|
|
||||||
field_height /= float(y_label_span) or 1 |
|
||||||
field_width = self.field_width() |
|
||||||
|
|
||||||
prev_sum = [0] * len(self.fields) |
|
||||||
cum_sum = [-min_value] * len(self.fields) |
|
||||||
|
|
||||||
coord_format = lambda c: '%(x)s %(y)s' % c |
|
||||||
lines = node(self.graph, "g", {'class': 'lines'}) |
|
||||||
for line_n, data in reversed(list(enumerate(self.data))): |
|
||||||
if not self.stacked: |
|
||||||
cum_sum = [-min_value] * len(self.fields) |
|
||||||
|
|
||||||
cum_sum = map(add, cum_sum, data['data']) |
|
||||||
get_coords = lambda (i, val): self.calc_coords( |
|
||||||
i, val, field_width, field_height) |
|
||||||
coords = map(get_coords, enumerate(cum_sum)) |
|
||||||
paths = map(coord_format, coords) |
|
||||||
line_path = ' '.join(paths) |
|
||||||
linegroup = node(lines, "g", {'class': 'linegroup%d' % line_n}) |
|
||||||
|
|
||||||
if self.area_fill: |
|
||||||
# to draw the area, we'll use the line above, followed by |
|
||||||
# tracing the bottom from right to left |
|
||||||
if self.stacked: |
|
||||||
prev_sum_rev = list(enumerate(prev_sum)).reversed() |
|
||||||
coords = map(get_coords, prev_sum_rev) |
|
||||||
paths = map(coord_format, coords) |
|
||||||
area_path = ' '.join(paths) |
|
||||||
origin = paths[-1] |
|
||||||
else: |
|
||||||
area_path = "V%s" % self.graph_height |
|
||||||
origin = coord_format(get_coords((0, 0))) |
|
||||||
|
|
||||||
node(linegroup, 'path', { |
|
||||||
'class': 'fill fill%s' % line_n, |
|
||||||
'd': ' '.join( |
|
||||||
('M', origin, 'L', line_path, area_path, 'Z')), |
|
||||||
}) |
|
||||||
|
|
||||||
# now draw the line itself |
|
||||||
node(linegroup, 'path', { |
|
||||||
'd': 'M%s L%s' % (paths[0], line_path), |
|
||||||
'class': 'line line%s' % line_n, |
|
||||||
}) |
|
||||||
dots = node(linegroup, "g", |
|
||||||
{'class': 'dots'}) |
|
||||||
if self.show_data_points or self.show_data_values: |
|
||||||
for i, value in enumerate(cum_sum): |
|
||||||
dot = node(dots, "g", |
|
||||||
{'class': 'dot'}) |
|
||||||
if self.show_data_points: |
|
||||||
node( |
|
||||||
dot, |
|
||||||
'circle', |
|
||||||
{'class': 'dot%s' % line_n}, |
|
||||||
cx=str(field_width * i), |
|
||||||
cy=str(self.graph_height - value * field_height), |
|
||||||
r='5', |
|
||||||
) |
|
||||||
self.make_datapoint_text( |
|
||||||
dot, |
|
||||||
field_width * i, |
|
||||||
self.graph_height - value * field_height - 6, |
|
||||||
value + min_value |
|
||||||
) |
|
||||||
prev_sum = list(cum_sum) |
|
@ -1,280 +0,0 @@ |
|||||||
import math |
|
||||||
import itertools |
|
||||||
from pygal.util import node |
|
||||||
from pygal.graph import Graph |
|
||||||
|
|
||||||
|
|
||||||
def robust_add(a, b): |
|
||||||
"Add numbers a and b, treating None as 0" |
|
||||||
if a is None: |
|
||||||
a = 0 |
|
||||||
if b is None: |
|
||||||
b = 0 |
|
||||||
return a + b |
|
||||||
|
|
||||||
RADIANS = math.pi / 180 |
|
||||||
|
|
||||||
|
|
||||||
class Pie(Graph): |
|
||||||
""" |
|
||||||
A presentation-quality SVG pie graph |
|
||||||
|
|
||||||
Synopsis |
|
||||||
======== |
|
||||||
|
|
||||||
from pygal.pie import Pie |
|
||||||
fields = ['Jan', 'Feb', 'Mar'] |
|
||||||
|
|
||||||
data_sales_02 = [12, 45, 21] |
|
||||||
|
|
||||||
graph = Pie(dict( |
|
||||||
height = 500, |
|
||||||
width = 300, |
|
||||||
fields = fields)) |
|
||||||
graph.add_data({'data': data_sales_02, 'title': 'Sales 2002'}) |
|
||||||
print "Content-type" image/svg+xml\r\n\r\n' |
|
||||||
print graph.burn() |
|
||||||
|
|
||||||
Description |
|
||||||
=========== |
|
||||||
This object aims to allow you to easily create high quality |
|
||||||
SVG pie graphs. You can either use the default style sheet |
|
||||||
or supply your own. Either way there are many options which can |
|
||||||
be configured to give you control over how the graph is |
|
||||||
generated - with or without a key, display percent on pie chart, |
|
||||||
title, subtitle etc. |
|
||||||
""" |
|
||||||
|
|
||||||
"if true, displays a drop shadow for the chart" |
|
||||||
show_shadow = False |
|
||||||
"Sets the offset of the shadow from the pie chart" |
|
||||||
shadow_offset = 10 |
|
||||||
|
|
||||||
show_data_labels = True |
|
||||||
"If true, display the actual field values in the data labels" |
|
||||||
show_actual_values = False |
|
||||||
|
|
||||||
("If true, display the percentage value of" |
|
||||||
"each pie wedge in the data labels") |
|
||||||
show_percent = True |
|
||||||
|
|
||||||
"If true, display the labels in the key" |
|
||||||
show_key_data_labels = True |
|
||||||
"If true, display the actual value of the field in the key" |
|
||||||
show_key_actual_values = True |
|
||||||
"If true, display the percentage value of the wedges in the key" |
|
||||||
show_key_percent = False |
|
||||||
|
|
||||||
"If true, explode the pie (put space between the wedges)" |
|
||||||
expanded = False |
|
||||||
"If true, expand the largest pie wedge" |
|
||||||
expand_greatest = False |
|
||||||
"The amount of space between expanded wedges" |
|
||||||
expand_gap = 10 |
|
||||||
|
|
||||||
show_x_labels = False |
|
||||||
show_y_labels = False |
|
||||||
|
|
||||||
"The font size of the data point labels" |
|
||||||
datapoint_font_size = 12 |
|
||||||
|
|
||||||
stylesheet_names = Graph.stylesheet_names + ['pie.css'] |
|
||||||
|
|
||||||
def add_data(self, data_descriptor): |
|
||||||
""" |
|
||||||
Add a data set to the graph |
|
||||||
|
|
||||||
>>> graph.add_data({data:[1,2,3,4]}) # doctest: +SKIP |
|
||||||
|
|
||||||
Note that a 'title' key is ignored. |
|
||||||
|
|
||||||
Multiple calls to add_data will sum the elements, and the pie will |
|
||||||
display the aggregated data. e.g. |
|
||||||
|
|
||||||
>>> graph.add_data({data:[1,2,3,4]}) # doctest: +SKIP |
|
||||||
>>> graph.add_data({data:[2,3,5,7]}) # doctest: +SKIP |
|
||||||
|
|
||||||
is the same as: |
|
||||||
|
|
||||||
>>> graph.add_data({data:[3,5,8,11]}) # doctest: +SKIP |
|
||||||
|
|
||||||
If data is added of with differing lengths, the corresponding |
|
||||||
values will be assumed to be zero. |
|
||||||
|
|
||||||
>>> graph.add_data({data:[1,2,3,4]}) # doctest: +SKIP |
|
||||||
>>> graph.add_data({data:[5,7]}) # doctest: +SKIP |
|
||||||
|
|
||||||
is the same as: |
|
||||||
|
|
||||||
>>> graph.add_data({data:[5,7]}) # doctest: +SKIP |
|
||||||
>>> graph.add_data({data:[1,2,3,4]}) # doctest: +SKIP |
|
||||||
|
|
||||||
and |
|
||||||
|
|
||||||
>>> graph.add_data({data:[6,9,3,4]}) # doctest: +SKIP |
|
||||||
""" |
|
||||||
pairs = itertools.izip_longest(self.data, data_descriptor['data']) |
|
||||||
self.data = list(itertools.starmap(robust_add, pairs)) |
|
||||||
|
|
||||||
# def add_defs(self, defs): |
|
||||||
# "Add svg definitions" |
|
||||||
# node( |
|
||||||
# defs, |
|
||||||
# 'filter', |
|
||||||
# id='dropshadow', |
|
||||||
# width='1.2', |
|
||||||
# height='1.2', |
|
||||||
# ) |
|
||||||
# node( |
|
||||||
# defs, |
|
||||||
# 'feGaussianBlur', |
|
||||||
# stdDeviation='4', |
|
||||||
# result='blur', |
|
||||||
# ) |
|
||||||
|
|
||||||
def draw_graph(self): |
|
||||||
"Here we don't need the graph (consider refactoring)" |
|
||||||
pass |
|
||||||
|
|
||||||
def get_y_labels(self): |
|
||||||
"Definitely consider refactoring" |
|
||||||
return [''] |
|
||||||
|
|
||||||
def get_x_labels(self): |
|
||||||
"Okay. I'll refactor after this" |
|
||||||
return [''] |
|
||||||
|
|
||||||
def keys(self): |
|
||||||
total = sum(self.data) |
|
||||||
|
|
||||||
def key(field, value): |
|
||||||
result = [field] |
|
||||||
result.append('[%s]' % value) |
|
||||||
if self.show_key_percent: |
|
||||||
percent = str(round((value / total * 100))) + '%' |
|
||||||
result.append(percent) |
|
||||||
return ' '.join(result) |
|
||||||
return map(key, self.fields, self.data) |
|
||||||
|
|
||||||
def draw_data(self): |
|
||||||
self.graph = node(self.root, 'g') |
|
||||||
background = node(self.graph, 'g') |
|
||||||
# midground is somewhere between the background and the foreground |
|
||||||
midground = node(self.graph, 'g') |
|
||||||
|
|
||||||
is_expanded = (self.expanded or self.expand_greatest) |
|
||||||
diameter = min(self.graph_width, self.graph_height) |
|
||||||
# the following assumes int(True)==1 and int(False)==0 |
|
||||||
diameter -= self.expand_gap * int(is_expanded) |
|
||||||
diameter -= self.datapoint_font_size * int(self.show_data_labels) |
|
||||||
diameter -= 10 * int(self.show_shadow) |
|
||||||
radius = diameter / 2.0 |
|
||||||
|
|
||||||
xoff = (self.width - diameter) / 2 |
|
||||||
yoff = (self.height - self.border_bottom - diameter) |
|
||||||
yoff -= 10 * int(self.show_shadow) |
|
||||||
transform = 'translate(%s %s)' % (xoff, yoff) |
|
||||||
self.graph.set('transform', transform) |
|
||||||
|
|
||||||
total = sum(self.data) |
|
||||||
max_value = max(self.data) |
|
||||||
|
|
||||||
percent_scale = 100.0 / total |
|
||||||
|
|
||||||
prev_percent = 0 |
|
||||||
rad_mult = 3.6 * RADIANS |
|
||||||
for index, (field, value) in enumerate(zip(self.fields, self.data)): |
|
||||||
percent = percent_scale * value |
|
||||||
|
|
||||||
radians = prev_percent * rad_mult |
|
||||||
x_start = radius + (math.sin(radians) * radius) |
|
||||||
y_start = radius - (math.cos(radians) * radius) |
|
||||||
radians = (prev_percent + percent) * rad_mult |
|
||||||
x_end = radius + (math.sin(radians) * radius) |
|
||||||
y_end = radius - (math.cos(radians) * radius) |
|
||||||
percent_greater_fifty = int(percent >= 50) |
|
||||||
path = "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_group = node(self.foreground, "g", |
|
||||||
{'class': 'pie'}) |
|
||||||
wedge = node( |
|
||||||
wedge_group, |
|
||||||
'path', { |
|
||||||
'd': path, |
|
||||||
'class': 'fill fill%s' % (index + 1)} |
|
||||||
) |
|
||||||
|
|
||||||
translate = None |
|
||||||
tx = 0 |
|
||||||
ty = 0 |
|
||||||
half_percent = prev_percent + percent / 2 |
|
||||||
radians = half_percent * rad_mult |
|
||||||
|
|
||||||
if self.show_shadow: |
|
||||||
shadow = node( |
|
||||||
background, |
|
||||||
'path', |
|
||||||
d=path, |
|
||||||
filter='url(#dropshadow)', |
|
||||||
style='fill: #ccc; stroke: none', |
|
||||||
) |
|
||||||
clear = node( |
|
||||||
midground, |
|
||||||
'path', |
|
||||||
d=path, |
|
||||||
# note, this probably only works when the background |
|
||||||
# is also #fff |
|
||||||
# consider getting the style from the stylesheet |
|
||||||
style="fill:#fff; stroke:none;", |
|
||||||
) |
|
||||||
|
|
||||||
if self.expanded or (self.expand_greatest and value == max_value): |
|
||||||
tx = (math.sin(radians) * self.expand_gap) |
|
||||||
ty = -(math.cos(radians) * self.expand_gap) |
|
||||||
translate = "translate(%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(%s %s)' % (shadow_tx, shadow_ty) |
|
||||||
shadow.set('transform', translate) |
|
||||||
|
|
||||||
if self.show_data_labels and value != 0: |
|
||||||
label = [] |
|
||||||
if self.show_key_data_labels: |
|
||||||
label.append(field) |
|
||||||
if self.show_actual_values: |
|
||||||
label.append('[%s]' % value) |
|
||||||
if self.show_percent: |
|
||||||
label.append('%d%%' % round(percent)) |
|
||||||
label = ' '.join(label) |
|
||||||
|
|
||||||
msr = math.sin(radians) |
|
||||||
mcr = math.cos(radians) |
|
||||||
tx = radius + (msr * radius) |
|
||||||
ty = radius - (mcr * radius) |
|
||||||
|
|
||||||
if self.expanded or ( |
|
||||||
self.expand_greatest and value == max_value): |
|
||||||
tx += (msr * self.expand_gap) |
|
||||||
ty -= (mcr * self.expand_gap) |
|
||||||
|
|
||||||
label_node = node( |
|
||||||
wedge_group, |
|
||||||
'text', |
|
||||||
{ |
|
||||||
'x': tx, |
|
||||||
'y': ty, |
|
||||||
'class': 'dataPointLabel', |
|
||||||
} |
|
||||||
) |
|
||||||
label_node.text = label |
|
||||||
|
|
||||||
prev_percent += percent |
|
||||||
|
|
||||||
def round(self, val, to): |
|
||||||
return round(val, to) |
|
@ -1,276 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
|
|
||||||
"plot.py" |
|
||||||
|
|
||||||
import sys |
|
||||||
from itertools import izip, count, chain |
|
||||||
from lxml import etree |
|
||||||
|
|
||||||
from pygal.util import node, float_range |
|
||||||
from pygal.graph import Graph |
|
||||||
|
|
||||||
|
|
||||||
def get_pairs(i): |
|
||||||
i = iter(i) |
|
||||||
while True: |
|
||||||
yield i.next(), i.next() |
|
||||||
|
|
||||||
# I'm not sure how this is more beautiful than ugly. |
|
||||||
if sys.version >= '3': |
|
||||||
def apply(func): |
|
||||||
return func() |
|
||||||
|
|
||||||
|
|
||||||
class Plot(Graph): |
|
||||||
"""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, ... |
|
||||||
scale_y_divisions = None |
|
||||||
# Make the X axis labels integers |
|
||||||
scale_x_integers = False |
|
||||||
# Make the Y axis labels integers |
|
||||||
scale_y_integers = False |
|
||||||
# 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_data_points = True |
|
||||||
# Indicate whether the lines should be drawn between points |
|
||||||
draw_lines_between_points = True |
|
||||||
# Set the minimum value of the X axis |
|
||||||
min_x_value = None |
|
||||||
# Set the minimum value of the Y axis |
|
||||||
min_y_value = None |
|
||||||
# Set the maximum value of the X axis |
|
||||||
max_x_value = None |
|
||||||
# Set the maximum value of the Y axis |
|
||||||
max_y_value = None |
|
||||||
|
|
||||||
stacked = False |
|
||||||
|
|
||||||
stylesheet_names = Graph.stylesheet_names + ['plot.css'] |
|
||||||
|
|
||||||
@apply |
|
||||||
def scale_x_divisions(): |
|
||||||
"""Determines the scaling for the X axis divisions. |
|
||||||
|
|
||||||
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) |
|
||||||
|
|
||||||
def fset(self, val): |
|
||||||
self._scale_x_divisions = val |
|
||||||
return property(**locals()) |
|
||||||
|
|
||||||
def validate_data(self, data): |
|
||||||
if len(data['data']) % 2 != 0: |
|
||||||
raise ValueError( |
|
||||||
"Expecting x,y pairs for data points for %s." % |
|
||||||
self.__class__.__name__) |
|
||||||
|
|
||||||
def process_data(self, data): |
|
||||||
pairs = list(get_pairs(data['data'])) |
|
||||||
pairs.sort() |
|
||||||
data['data'] = zip(*pairs) |
|
||||||
|
|
||||||
def calculate_left_margin(self): |
|
||||||
super(Plot, self).calculate_left_margin() |
|
||||||
label_left = len(str( |
|
||||||
self.get_x_labels()[0])) / 2 * self.font_size * 0.6 |
|
||||||
self.border_left = max(label_left, self.border_left) |
|
||||||
|
|
||||||
def calculate_right_margin(self): |
|
||||||
super(Plot, self).calculate_right_margin() |
|
||||||
label_right = len(str( |
|
||||||
self.get_x_labels()[-1])) / 2 * self.font_size * 0.6 |
|
||||||
self.border_right = max(label_right, self.border_right) |
|
||||||
|
|
||||||
def data_max(self, axis): |
|
||||||
data_index = getattr(self, '%s_data_index' % axis) |
|
||||||
max_value = max(chain( |
|
||||||
*map(lambda set: set['data'][data_index], self.data))) |
|
||||||
# above is same as |
|
||||||
#max_value = max(map(lambda set: |
|
||||||
# max(set['data'][data_index]), self.data)) |
|
||||||
spec_max = getattr(self, 'max_%s_value' % axis) |
|
||||||
# Python 3 doesn't allow comparing None to int, so use -∞ |
|
||||||
if spec_max is None: |
|
||||||
spec_max = float('-Inf') |
|
||||||
max_value = max(max_value, spec_max) |
|
||||||
return max_value |
|
||||||
|
|
||||||
def data_min(self, axis): |
|
||||||
data_index = getattr(self, '%s_data_index' % axis) |
|
||||||
min_value = min(chain( |
|
||||||
*map(lambda set: set['data'][data_index], self.data))) |
|
||||||
spec_min = getattr(self, 'min_%s_value' % axis) |
|
||||||
if spec_min is not None: |
|
||||||
min_value = min(min_value, spec_min) |
|
||||||
return min_value |
|
||||||
|
|
||||||
x_data_index = 0 |
|
||||||
y_data_index = 1 |
|
||||||
|
|
||||||
def data_range(self, axis): |
|
||||||
side = {'x': 'right', 'y': 'top'}[axis] |
|
||||||
|
|
||||||
min_value = self.data_min(axis) |
|
||||||
max_value = self.data_max(axis) |
|
||||||
range = max_value - min_value |
|
||||||
|
|
||||||
side_pad = range / 20.0 or 10 |
|
||||||
scale_range = (max_value + side_pad) - min_value |
|
||||||
|
|
||||||
scale_division = getattr( |
|
||||||
self, 'scale_%s_divisions' % axis) or (scale_range / 10.0) |
|
||||||
|
|
||||||
if getattr(self, 'scale_%s_integers' % axis): |
|
||||||
scale_division = round(scale_division) or 1 |
|
||||||
|
|
||||||
return min_value, max_value, scale_division |
|
||||||
|
|
||||||
def x_range(self): |
|
||||||
return self.data_range('x') |
|
||||||
|
|
||||||
def y_range(self): |
|
||||||
return self.data_range('y') |
|
||||||
|
|
||||||
def get_data_values(self, axis): |
|
||||||
min_value, max_value, scale_division = self.data_range(axis) |
|
||||||
return tuple(float_range(*self.data_range(axis))) |
|
||||||
|
|
||||||
def get_x_values(self): |
|
||||||
return self.get_data_values('x') |
|
||||||
|
|
||||||
def get_y_values(self): |
|
||||||
return self.get_data_values('y') |
|
||||||
|
|
||||||
def get_x_labels(self): |
|
||||||
return map(str, self.get_x_values()) |
|
||||||
|
|
||||||
def get_y_labels(self): |
|
||||||
return map(str, self.get_y_values()) |
|
||||||
|
|
||||||
def field_size(self, axis): |
|
||||||
size = {'x': 'width', 'y': 'height'}[axis] |
|
||||||
side = {'x': 'right', 'y': 'top'}[axis] |
|
||||||
values = getattr(self, 'get_%s_values' % axis)() |
|
||||||
max_d = self.data_max(axis) |
|
||||||
dx = ( |
|
||||||
float(max_d - values[-1]) / (values[-1] - values[-2]) |
|
||||||
if len(values) > 1 else max_d |
|
||||||
) |
|
||||||
graph_size = getattr(self, 'graph_%s' % size) |
|
||||||
side_font = getattr(self, '%s_font' % side) |
|
||||||
side_align = getattr(self, '%s_align' % side) |
|
||||||
result = ((float(graph_size) - self.font_size * 2 * side_font) / |
|
||||||
(len(values) + dx - side_align)) |
|
||||||
return result |
|
||||||
|
|
||||||
def field_width(self): |
|
||||||
return self.field_size('x') |
|
||||||
|
|
||||||
def field_height(self): |
|
||||||
return self.field_size('y') |
|
||||||
|
|
||||||
def draw_data(self): |
|
||||||
self.load_transform_parameters() |
|
||||||
for line, data in izip(count(1), self.data): |
|
||||||
x_start, y_start = self.transform_output_coordinates( |
|
||||||
(data['data'][self.x_data_index][0], |
|
||||||
data['data'][self.y_data_index][0]) |
|
||||||
) |
|
||||||
data_points = zip(*data['data']) |
|
||||||
graph_points = self.get_graph_points(data_points) |
|
||||||
lpath = self.get_lpath(graph_points) |
|
||||||
if self.area_fill: |
|
||||||
graph_height = self.graph_height |
|
||||||
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: |
|
||||||
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 |
|
||||||
|
|
||||||
def add_constant_line(self, value, label=None, style=None): |
|
||||||
self.constant_lines = getattr(self, 'constant_lines', []) |
|
||||||
self.constant_lines.append((value, label, style)) |
|
||||||
|
|
||||||
def _draw_constant_lines(self): |
|
||||||
if hasattr(self, 'constant_lines'): |
|
||||||
map(self.__draw_constant_line, self.constant_lines) |
|
||||||
|
|
||||||
def __draw_constant_line(self, value_label_style): |
|
||||||
"Draw a constant line on the y-axis with the label" |
|
||||||
value, label, style = value_label_style |
|
||||||
start = self.transform_output_coordinates((0, value))[1] |
|
||||||
stop = self.graph_width |
|
||||||
path = node(self.graph, 'path', { |
|
||||||
'd': 'M 0 %s h%s' % (start, stop), |
|
||||||
'class': 'constantLine'}) |
|
||||||
if style: |
|
||||||
path.set('style', style) |
|
||||||
text = node(self.graph, 'text', { |
|
||||||
'x': 2, |
|
||||||
'y': start - 2, |
|
||||||
'class': 'constantLine'}) |
|
||||||
text.text = label |
|
||||||
|
|
||||||
def load_transform_parameters(self): |
|
||||||
"Cache the parameters necessary to transform x & y coordinates" |
|
||||||
x_min, x_max, x_div = self.x_range() |
|
||||||
y_min, y_max, y_div = self.y_range() |
|
||||||
x_step = ((float(self.graph_width) - self.font_size * 2) / |
|
||||||
(x_max - x_min)) |
|
||||||
y_step = ((float(self.graph_height) - self.font_size * 2) / |
|
||||||
(y_max - y_min)) |
|
||||||
self.__transform_parameters = dict(vars()) |
|
||||||
del self.__transform_parameters['self'] |
|
||||||
|
|
||||||
def get_graph_points(self, data_points): |
|
||||||
return map(self.transform_output_coordinates, data_points) |
|
||||||
|
|
||||||
def get_lpath(self, points): |
|
||||||
points = map(lambda p: "%f %f" % p, points) |
|
||||||
return 'L' + ' '.join(points) |
|
||||||
|
|
||||||
def transform_output_coordinates(self, (x, y)): |
|
||||||
x_min = self.__transform_parameters['x_min'] |
|
||||||
x_step = self.__transform_parameters['x_step'] |
|
||||||
y_min = self.__transform_parameters['y_min'] |
|
||||||
y_step = self.__transform_parameters['y_step'] |
|
||||||
x = (x - x_min) * x_step |
|
||||||
y = self.graph_height - (y - y_min) * y_step |
|
||||||
return x, y |
|
||||||
|
|
||||||
def draw_data_points(self, line, data_points, graph_points): |
|
||||||
if not self.show_data_points and not self.show_data_values: |
|
||||||
return |
|
||||||
|
|
||||||
for ((dx, dy), (gx, gy)) in izip(data_points, graph_points): |
|
||||||
if self.show_data_points: |
|
||||||
node(self.graph, 'circle', { |
|
||||||
'cx': gx, |
|
||||||
'cy': gy, |
|
||||||
'r': '2.5', |
|
||||||
'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) |
|
||||||
|
|
||||||
def format(self, x, y): |
|
||||||
return '(%0.2f, %0.2f)' % (x, y) |
|
@ -1,216 +0,0 @@ |
|||||||
# -*- 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 |
|
||||||
|
|
||||||
__all__ = ('Schedule') |
|
||||||
|
|
||||||
|
|
||||||
class Schedule(Graph): |
|
||||||
""" |
|
||||||
Graph of temporal scalar data. |
|
||||||
""" |
|
||||||
|
|
||||||
# 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' |
|
||||||
timescale_divisions = None |
|
||||||
|
|
||||||
# The formatting used for the popups. See x_label_format |
|
||||||
popup_format = '%Y-%m-%d %H:%M:%S' |
|
||||||
|
|
||||||
_min_x_value = None |
|
||||||
scale_x_divisions = False |
|
||||||
scale_x_integers = False |
|
||||||
bar_gap = True |
|
||||||
|
|
||||||
stylesheet_names = Graph.stylesheet_names + ['bar.css'] |
|
||||||
|
|
||||||
def add_data(self, data): |
|
||||||
""" |
|
||||||
Add data to the plot. |
|
||||||
Note that the data must be in time,value pairs, |
|
||||||
and that the date format |
|
||||||
may be any date that is parseable by ParseDate. |
|
||||||
Also note that, in this example, we're mixing scales; the data from d1 |
|
||||||
will probably not be discernable if both data sets are |
|
||||||
plotted on the same graph, since d1 is too granular. |
|
||||||
""" |
|
||||||
# The ruby version does something different here, throwing out |
|
||||||
# any previously added data. |
|
||||||
super(Schedule, self).add_data(data) |
|
||||||
|
|
||||||
# copied from Bar |
|
||||||
# TODO, refactor this into a common base class (or mix-in) |
|
||||||
def get_bar_gap(self, field_size): |
|
||||||
bar_gap = 10 # default gap |
|
||||||
if field_size < 10: |
|
||||||
# adjust for narrow fields |
|
||||||
bar_gap = field_size / 2 |
|
||||||
# the following zero's out the gap if bar_gap is False |
|
||||||
bar_gap = int(self.bar_gap) * bar_gap |
|
||||||
return bar_gap |
|
||||||
|
|
||||||
def validate_data(self, conf): |
|
||||||
super(Schedule, self).validate_data(conf) |
|
||||||
msg = "Data supplied must be (title, from, to) tripples!" |
|
||||||
assert len(conf['data']) % 3 == 0, msg |
|
||||||
|
|
||||||
def process_data(self, conf): |
|
||||||
super(Schedule, self).process_data(conf) |
|
||||||
data = conf['data'] |
|
||||||
triples = grouper(3, data) |
|
||||||
|
|
||||||
labels, begin_dates, end_dates = zip(*triples) |
|
||||||
|
|
||||||
begin_dates = map(self.parse_date, begin_dates) |
|
||||||
end_dates = map(self.parse_date, end_dates) |
|
||||||
|
|
||||||
# reconstruct the triples in a new order |
|
||||||
reordered_triples = zip(begin_dates, end_dates, labels) |
|
||||||
|
|
||||||
# because of the reordering, this will sort by begin_date |
|
||||||
# then end_date, then label. |
|
||||||
reordered_triples.sort() |
|
||||||
|
|
||||||
conf['data'] = reordered_triples |
|
||||||
|
|
||||||
def parse_date(self, date_string): |
|
||||||
return parse(date_string) |
|
||||||
|
|
||||||
def set_min_x_value(self, value): |
|
||||||
if isinstance(value, basestring): |
|
||||||
value = self.parse_date(value) |
|
||||||
self._min_x_value = value |
|
||||||
|
|
||||||
def get_min_x_value(self): |
|
||||||
return self._min_x_value |
|
||||||
|
|
||||||
min_x_value = property(get_min_x_value, set_min_x_value) |
|
||||||
|
|
||||||
def format(self, x, y): |
|
||||||
return x.strftime(self.popup_format) |
|
||||||
|
|
||||||
def get_x_labels(self): |
|
||||||
format = lambda x: x.strftime(self.x_label_format) |
|
||||||
return map(format, self.get_x_values()) |
|
||||||
|
|
||||||
def y_label_offset(self, height): |
|
||||||
return height / -2.0 |
|
||||||
|
|
||||||
def get_y_labels(self): |
|
||||||
# ruby version uses the last data supplied |
|
||||||
last = -1 |
|
||||||
data = self.data[last]['data'] |
|
||||||
begin_dates, start_dates, labels = zip(*data) |
|
||||||
return labels |
|
||||||
|
|
||||||
def draw_data(self): |
|
||||||
bar_gap = self.get_bar_gap(self.get_field_height()) |
|
||||||
|
|
||||||
subbar_height = self.get_field_height() - bar_gap |
|
||||||
|
|
||||||
x_min, x_max, div = self._x_range() |
|
||||||
x_range = x_max - x_min |
|
||||||
width = (float(self.graph_width) - self.font_size * 2) |
|
||||||
# time_scale |
|
||||||
#scale /= x_range |
|
||||||
scale = TimeScale(width, x_range) |
|
||||||
|
|
||||||
# ruby version uses the last data supplied |
|
||||||
last = -1 |
|
||||||
data = self.data[last]['data'] |
|
||||||
|
|
||||||
for index, (x_start, x_end, label) in enumerate(data): |
|
||||||
count = index + 1 # index is 0-based, count is 1-based |
|
||||||
y = self.graph_height - (self.get_field_height() * count) |
|
||||||
bar_width = scale * (x_end - x_start) |
|
||||||
bar_start = scale * (x_start - x_min) |
|
||||||
|
|
||||||
node(self.graph, 'rect', { |
|
||||||
'x': bar_start, |
|
||||||
'y': y, |
|
||||||
'width': bar_width, |
|
||||||
'height': subbar_height, |
|
||||||
'class': 'fill%s' % (count + 1), |
|
||||||
}) |
|
||||||
|
|
||||||
def _x_range(self): |
|
||||||
# ruby version uses teh last data supplied |
|
||||||
last = -1 |
|
||||||
data = self.data[last]['data'] |
|
||||||
|
|
||||||
start_dates, end_dates, labels = zip(*data) |
|
||||||
all_dates = start_dates + end_dates |
|
||||||
max_value = max(all_dates) |
|
||||||
if not self.min_x_value is None: |
|
||||||
all_dates.append(self.min_x_value) |
|
||||||
min_value = min(all_dates) |
|
||||||
range = max_value - min_value |
|
||||||
right_pad = divide_timedelta_float( |
|
||||||
range, 20.0) or relativedelta(days=10) |
|
||||||
scale_range = (max_value + right_pad) - min_value |
|
||||||
|
|
||||||
#scale_division = self.scale_x_divisions or (scale_range / 10.0) |
|
||||||
# todo, remove timescale_x_divisions and use scale_x_divisions only |
|
||||||
# but as a time delta |
|
||||||
scale_division = divide_timedelta_float(scale_range, 10.0) |
|
||||||
|
|
||||||
# this doesn't make sense, because x is a timescale |
|
||||||
#if self.scale_x_integers: |
|
||||||
# scale_division = min(round(scale_division), 1) |
|
||||||
|
|
||||||
return min_value, max_value, scale_division |
|
||||||
|
|
||||||
def get_x_values(self): |
|
||||||
x_min, x_max, scale_division = self._x_range() |
|
||||||
if self.timescale_divisions: |
|
||||||
pattern = re.compile('(\d+) ?(\w+)') |
|
||||||
m = pattern.match(self.timescale_divisions) |
|
||||||
if not m: |
|
||||||
raise (ValueError, |
|
||||||
"Invalid timescale_divisions: %s" % |
|
||||||
self.timescale_divisions) |
|
||||||
|
|
||||||
magnitude = int(m.group(1)) |
|
||||||
units = m.group(2) |
|
||||||
|
|
||||||
parameter = self.lookup_relativedelta_parameter(units) |
|
||||||
|
|
||||||
delta = relativedelta(**{parameter: magnitude}) |
|
||||||
|
|
||||||
scale_division = delta |
|
||||||
|
|
||||||
return date_range(x_min, x_max, scale_division) |
|
||||||
|
|
||||||
def lookup_relativedelta_parameter(self, unit_string): |
|
||||||
from util import reverse_mapping, flatten_mapping |
|
||||||
unit_string = unit_string.lower() |
|
||||||
mapping = dict( |
|
||||||
years=('years', 'year', 'yrs', 'yr'), |
|
||||||
months=('months', 'month', 'mo'), |
|
||||||
weeks=('weeks', 'week', 'wks', 'wk'), |
|
||||||
days=('days', 'day'), |
|
||||||
hours=('hours', 'hour', 'hr', 'hrs', 'h'), |
|
||||||
minutes=('minutes', 'minute', 'min', 'mins', 'm'), |
|
||||||
seconds=('seconds', 'second', 'sec', 'secs', 's'), |
|
||||||
) |
|
||||||
mapping = reverse_mapping(mapping) |
|
||||||
mapping = flatten_mapping(mapping) |
|
||||||
if not unit_string in mapping: |
|
||||||
raise ValueError("%s doesn't match any supported time/date unit") |
|
||||||
return mapping[unit_string] |
|
@ -1,199 +0,0 @@ |
|||||||
#!/usr/bin/env python |
|
||||||
import pygal.plot |
|
||||||
import re |
|
||||||
import pkg_resources |
|
||||||
pkg_resources.require("python-dateutil>=1.1") |
|
||||||
from dateutil.parser import parse |
|
||||||
from dateutil.relativedelta import relativedelta |
|
||||||
from time import mktime |
|
||||||
import datetime |
|
||||||
fromtimestamp = datetime.datetime.fromtimestamp |
|
||||||
from .util import float_range |
|
||||||
|
|
||||||
|
|
||||||
class Plot(pygal.plot.Plot): |
|
||||||
"""=== For creating SVG plots of scalar temporal data |
|
||||||
|
|
||||||
= Synopsis |
|
||||||
|
|
||||||
import SVG.TimeSeries |
|
||||||
|
|
||||||
# Data sets are x,y pairs |
|
||||||
data1 = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11, |
|
||||||
"9/11/01", 9, "9/1/85", 2, |
|
||||||
"9/1/88", 1, "1/15/95", 13] |
|
||||||
data2 = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4, |
|
||||||
"5/1/02", 14, "3/1/95", 6, |
|
||||||
"8/1/91", 12, "12/1/87", 6, |
|
||||||
"5/1/84", 17, "10/1/80", 12] |
|
||||||
|
|
||||||
graph = SVG::Graph::TimeSeries.new({ |
|
||||||
:width => 640, |
|
||||||
:height => 480, |
|
||||||
:graph_title => title, |
|
||||||
:show_graph_title => true, |
|
||||||
:no_css => true, |
|
||||||
:key => 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", |
|
||||||
:show_y_title => true, |
|
||||||
:y_title => "Ice Cream Cones", |
|
||||||
:y_title_text_direction => :bt, |
|
||||||
:stagger_x_labels => true, |
|
||||||
:x_label_format => "%m/%d/%y", |
|
||||||
}) |
|
||||||
|
|
||||||
graph.add_data({ |
|
||||||
:data => projection |
|
||||||
:title => 'Projected', |
|
||||||
}) |
|
||||||
|
|
||||||
graph.add_data({ |
|
||||||
:data => actual, |
|
||||||
:title => 'Actual', |
|
||||||
}) |
|
||||||
|
|
||||||
print graph.burn() |
|
||||||
|
|
||||||
= Description |
|
||||||
|
|
||||||
Produces a graph of temporal scalar data. |
|
||||||
|
|
||||||
= Examples |
|
||||||
|
|
||||||
http://www.germane-software/repositories/public/SVG/test/timeseries.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: |
|
||||||
|
|
||||||
["12:30", 2] # A data set with 1 point: ("12:30",2) |
|
||||||
["01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and |
|
||||||
# ("14:20",6) |
|
||||||
|
|
||||||
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 |
|
||||||
|
|
||||||
== Author |
|
||||||
|
|
||||||
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> |
|
||||||
|
|
||||||
Copyright 2004 Sean E. Russell |
|
||||||
This software is available under the Ruby license[LICENSE.txt] |
|
||||||
""" |
|
||||||
popup_format = x_label_format = '%Y-%m-%d %H:%M:%S' |
|
||||||
__doc_popup_format_ = ("The formatting usped for the popups." |
|
||||||
" See x_label_format") |
|
||||||
__doc_x_label_format_ = ("The format string used to format " |
|
||||||
"the X axis labels. See strftime.") |
|
||||||
|
|
||||||
timescale_divisions = None |
|
||||||
__doc_timescale_divisions_ = """Use this to set the spacing |
|
||||||
between dates on the axis. The value must be of the form |
|
||||||
"\d+ ?(days|weeks|months|years|hours|minutes|seconds)?" |
|
||||||
|
|
||||||
EG: |
|
||||||
|
|
||||||
graph.timescale_divisions = "2 weeks" |
|
||||||
|
|
||||||
will cause the chart to try to divide the X axis up into segments of |
|
||||||
two week periods.""" |
|
||||||
|
|
||||||
def add_data(self, data): |
|
||||||
"""Add data to the plot. |
|
||||||
d1 = ["12:30", 2] # A data set with 1 point: ("12:30",2) |
|
||||||
d2 = ["01:00",2, "14:20",6] # A data set with 2 points: |
|
||||||
# ("01:00",2) and ("14:20",6) |
|
||||||
graph.add_data( |
|
||||||
:data => d1, |
|
||||||
:title => 'One' |
|
||||||
) |
|
||||||
graph.add_data( |
|
||||||
:data => d2, |
|
||||||
:title => 'Two' |
|
||||||
) |
|
||||||
|
|
||||||
Note that the data must be in time,value pairs, |
|
||||||
and that the date format |
|
||||||
may be any date that is parseable by ParseDate.""" |
|
||||||
super(Plot, self).add_data(data) |
|
||||||
|
|
||||||
def process_data(self, data): |
|
||||||
super(Plot, self).process_data(data) |
|
||||||
# the date should be in the first element, so parse it out |
|
||||||
data['data'][0] = map(self.parse_date, data['data'][0]) |
|
||||||
|
|
||||||
_min_x_value = pygal.plot.Plot.min_x_value |
|
||||||
|
|
||||||
def get_min_x_value(self): |
|
||||||
return self._min_x_value |
|
||||||
|
|
||||||
def set_min_x_value(self, date): |
|
||||||
self._min_x_value = self.parse_date(date) |
|
||||||
min_x_value = property(get_min_x_value, set_min_x_value) |
|
||||||
|
|
||||||
def format(self, x, y): |
|
||||||
return fromtimestamp(x).strftime(self.popup_format) |
|
||||||
|
|
||||||
def get_x_labels(self): |
|
||||||
return map(lambda t: fromtimestamp(t).strftime(self.x_label_format), |
|
||||||
self.get_x_values()) |
|
||||||
|
|
||||||
def get_x_values(self): |
|
||||||
result = self.get_x_timescale_division_values() |
|
||||||
if result: |
|
||||||
return result |
|
||||||
return tuple(float_range(*self.x_range())) |
|
||||||
|
|
||||||
def get_x_timescale_division_values(self): |
|
||||||
if not self.timescale_divisions: |
|
||||||
return |
|
||||||
min, max, scale_division = self.x_range() |
|
||||||
m = re.match( |
|
||||||
'(?P<amount>\d+) ?(?P<division_units>' |
|
||||||
'days|weeks|months|years|hours|minutes|seconds)?', |
|
||||||
self.timescale_divisions) |
|
||||||
# copy amount and division_units into the local namespace |
|
||||||
division_units = m.groupdict()['division_units'] or 'days' |
|
||||||
amount = int(m.groupdict()['amount']) |
|
||||||
if not amount: |
|
||||||
return |
|
||||||
delta = relativedelta(**{division_units: amount}) |
|
||||||
result = tuple(self.get_time_range(min, max, delta)) |
|
||||||
return result |
|
||||||
|
|
||||||
def get_time_range(self, start, stop, delta): |
|
||||||
start, stop = map(fromtimestamp, (start, stop)) |
|
||||||
current = start |
|
||||||
while current <= stop: |
|
||||||
yield mktime(current.timetuple()) |
|
||||||
current += delta |
|
||||||
|
|
||||||
def parse_date(self, date_string): |
|
||||||
return mktime(parse(date_string).timetuple()) |
|
@ -1,187 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
from lxml import etree |
|
||||||
import itertools |
|
||||||
import datetime |
|
||||||
|
|
||||||
|
|
||||||
def node(parent, tag, params=None, **extras): |
|
||||||
"""Make a etree node""" |
|
||||||
params = params or {} |
|
||||||
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): |
|
||||||
""" |
|
||||||
>>> tuple(grouper(3, 'abcdefg', 'x')) |
|
||||||
(('a', 'b', 'c'), ('d', 'e', 'f'), ('g', 'x', 'x')) |
|
||||||
""" |
|
||||||
return itertools.izip( |
|
||||||
*[itertools.chain(iterable, |
|
||||||
itertools.repeat(padvalue, n - 1))] * n) |
|
||||||
|
|
||||||
|
|
||||||
def reverse_mapping(mapping): |
|
||||||
""" |
|
||||||
For every key, value pair, return the mapping for the |
|
||||||
equivalent value, key pair |
|
||||||
>>> reverse_mapping({'a': 'b'}) == {'b': 'a'} |
|
||||||
True |
|
||||||
""" |
|
||||||
keys, values = zip(*mapping.items()) |
|
||||||
return dict(zip(values, keys)) |
|
||||||
|
|
||||||
|
|
||||||
def flatten_mapping(mapping): |
|
||||||
""" |
|
||||||
For every key that has an __iter__ method, assign the values |
|
||||||
to a key for each. |
|
||||||
>>> flatten_mapping({'ab': 3, ('c','d'): 4}) == {'ab': 3, 'c': 4, 'd': 4} |
|
||||||
True |
|
||||||
""" |
|
||||||
return dict(flatten_items(mapping.items())) |
|
||||||
|
|
||||||
|
|
||||||
def flatten_items(items): |
|
||||||
for keys, value in items: |
|
||||||
if hasattr(keys, '__iter__'): |
|
||||||
for key in keys: |
|
||||||
yield (key, value) |
|
||||||
else: |
|
||||||
yield (keys, value) |
|
||||||
|
|
||||||
|
|
||||||
def float_range(start=0, stop=None, step=1): |
|
||||||
""" |
|
||||||
Much like the built-in function range, but accepts floats |
|
||||||
>>> tuple(float_range(0, 9, 1.5)) |
|
||||||
(0.0, 1.5, 3.0, 4.5, 6.0, 7.5) |
|
||||||
""" |
|
||||||
start = float(start) |
|
||||||
while start < stop: |
|
||||||
yield start |
|
||||||
start += step |
|
||||||
|
|
||||||
|
|
||||||
def date_range(start=None, stop=None, step=None): |
|
||||||
""" |
|
||||||
Much like the built-in function range, but works with dates |
|
||||||
>>> my_range = tuple(date_range(datetime.datetime(2005,12,21), datetime.datetime(2005,12,25))) |
|
||||||
>>> datetime.datetime(2005,12,21) in my_range |
|
||||||
True |
|
||||||
>>> datetime.datetime(2005,12,22) in my_range |
|
||||||
True |
|
||||||
>>> datetime.datetime(2005,12,25) in my_range |
|
||||||
False |
|
||||||
""" |
|
||||||
if step is None: |
|
||||||
step = datetime.timedelta(days=1) |
|
||||||
if start is None: |
|
||||||
start = datetime.datetime.now() |
|
||||||
while start < stop: |
|
||||||
yield start |
|
||||||
start += step |
|
||||||
|
|
||||||
|
|
||||||
# copied from jaraco.datetools |
|
||||||
def divide_timedelta_float(td, divisor): |
|
||||||
""" |
|
||||||
Meant to work around the limitation that Python datetime doesn't support |
|
||||||
floats as divisors or multiplicands to datetime objects |
|
||||||
>>> one_day = datetime.timedelta(days=1) |
|
||||||
>>> half_day = datetime.timedelta(days=.5) |
|
||||||
>>> divide_timedelta_float(one_day, 2.0) == half_day |
|
||||||
True |
|
||||||
>>> divide_timedelta_float(one_day, 2) == half_day |
|
||||||
False |
|
||||||
""" |
|
||||||
# td is comprised of days, seconds, microseconds |
|
||||||
dsm = [getattr(td, attr) for attr in ('days', 'seconds', 'microseconds')] |
|
||||||
dsm = map(lambda elem: elem / divisor, dsm) |
|
||||||
return datetime.timedelta(*dsm) |
|
||||||
|
|
||||||
|
|
||||||
def get_timedelta_total_microseconds(td): |
|
||||||
seconds = td.days * 86400 + td.seconds |
|
||||||
microseconds = td.microseconds + seconds * (10 ** 6) |
|
||||||
return microseconds |
|
||||||
|
|
||||||
|
|
||||||
def divide_timedelta(td1, td2): |
|
||||||
""" |
|
||||||
Get the ratio of two timedeltas |
|
||||||
>>> one_day = datetime.timedelta(days=1) |
|
||||||
>>> one_hour = datetime.timedelta(hours=1) |
|
||||||
>>> divide_timedelta(one_hour, one_day) == 1/24.0 |
|
||||||
True |
|
||||||
""" |
|
||||||
|
|
||||||
td1_total = float(get_timedelta_total_microseconds(td1)) |
|
||||||
td2_total = float(get_timedelta_total_microseconds(td2)) |
|
||||||
return td1_total / td2_total |
|
||||||
|
|
||||||
|
|
||||||
class TimeScale(object): |
|
||||||
"Describes a scale factor based on time instead of a scalar" |
|
||||||
def __init__(self, width, range): |
|
||||||
self.width = width |
|
||||||
self.range = range |
|
||||||
|
|
||||||
def __mul__(self, delta): |
|
||||||
scale = divide_timedelta(delta, self.range) |
|
||||||
return scale * self.width |
|
||||||
|
|
||||||
|
|
||||||
# the following three functions were copied from jaraco.util.iter_ |
|
||||||
# todo, factor out caching capability |
|
||||||
class iterable_test(dict): |
|
||||||
"Test objects for iterability, caching the result by type" |
|
||||||
def __init__(self, ignore_classes=(basestring,)): |
|
||||||
"""ignore_classes must include basestring, because if a string |
|
||||||
is iterable, so is a single character, and the routine runs |
|
||||||
into an infinite recursion""" |
|
||||||
assert basestring in ignore_classes, ( |
|
||||||
'basestring must be in ignore_classes') |
|
||||||
self.ignore_classes = ignore_classes |
|
||||||
|
|
||||||
def __getitem__(self, candidate): |
|
||||||
return dict.get(self, type(candidate)) or self._test(candidate) |
|
||||||
|
|
||||||
def _test(self, candidate): |
|
||||||
try: |
|
||||||
if isinstance(candidate, self.ignore_classes): |
|
||||||
raise TypeError |
|
||||||
iter(candidate) |
|
||||||
result = True |
|
||||||
except TypeError: |
|
||||||
result = False |
|
||||||
self[type(candidate)] = result |
|
||||||
return result |
|
||||||
|
|
||||||
|
|
||||||
def iflatten(subject, test=None): |
|
||||||
if test is None: |
|
||||||
test = iterable_test() |
|
||||||
if not test[subject]: |
|
||||||
yield subject |
|
||||||
else: |
|
||||||
for elem in subject: |
|
||||||
for subelem in iflatten(elem, test): |
|
||||||
yield subelem |
|
||||||
|
|
||||||
|
|
||||||
def flatten(subject, test=None): |
|
||||||
"""flatten an iterable with possible nested iterables. |
|
||||||
Adapted from |
|
||||||
http://mail.python.org/pipermail/python-list/2003-November/233971.html |
|
||||||
>>> flatten(['a','b',['c','d',['e','f'],'g'],'h']) == ['a','b','c','d','e','f','g','h'] |
|
||||||
True |
|
||||||
|
|
||||||
Note this will normally ignore string types as iterables. |
|
||||||
>>> flatten(['ab', 'c']) |
|
||||||
['ab', 'c'] |
|
||||||
""" |
|
||||||
return list(iflatten(subject, test)) |
|
@ -1,108 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
import math |
|
||||||
|
|
||||||
pi = math.pi |
|
||||||
|
|
||||||
|
|
||||||
def cos(angle): |
|
||||||
return math.cos(angle * pi / 180) |
|
||||||
|
|
||||||
|
|
||||||
def sin(angle): |
|
||||||
return math.sin(angle * pi / 180) |
|
||||||
|
|
||||||
|
|
||||||
def calculate_right_margin(graph): |
|
||||||
""" |
|
||||||
Calculate the margin in pixels to the right of the plot area, |
|
||||||
setting border_right. |
|
||||||
""" |
|
||||||
br = 7 |
|
||||||
if graph.key and graph.key_position == 'right': |
|
||||||
max_key_len = max(map(len, graph.keys())) |
|
||||||
br += max_key_len * graph.key_font_size * 0.6 |
|
||||||
br += graph.key_box_size |
|
||||||
br += 10 # Some padding around the box |
|
||||||
return br |
|
||||||
|
|
||||||
|
|
||||||
def calculate_top_margin(graph): |
|
||||||
""" |
|
||||||
Calculate the margin in pixels above the plot area, setting |
|
||||||
border_top. |
|
||||||
""" |
|
||||||
bt = 10 |
|
||||||
if graph.show_graph_title: |
|
||||||
bt += graph.title_font_size |
|
||||||
if graph.show_graph_subtitle: |
|
||||||
bt += graph.subtitle_font_size |
|
||||||
return bt |
|
||||||
|
|
||||||
|
|
||||||
def calculate_bottom_margin(graph): |
|
||||||
""" |
|
||||||
Calculate the margin in pixels below the plot area, setting |
|
||||||
border_bottom. |
|
||||||
""" |
|
||||||
bb = 7 |
|
||||||
if graph.key and graph.key_position == 'bottom': |
|
||||||
bb += len(graph.data) * (graph.font_size + 5) |
|
||||||
bb += 10 |
|
||||||
if graph.show_x_labels: |
|
||||||
max_x_label_height_px = graph.x_label_font_size |
|
||||||
if graph.x_label_rotation: |
|
||||||
label_lengths = map(len, graph.get_x_labels()) |
|
||||||
max_x_label_len = reduce(max, label_lengths, 0) |
|
||||||
max_x_label_height_px *= max_x_label_len * 0.6 |
|
||||||
max_x_label_height_px *= sin(graph.x_label_rotation) |
|
||||||
bb += max_x_label_height_px + graph.y_label_font_size |
|
||||||
if graph.stagger_x_labels: |
|
||||||
bb += max_x_label_height_px + 10 |
|
||||||
if graph.show_x_title: |
|
||||||
bb += graph.x_title_font_size + 5 |
|
||||||
return bb |
|
||||||
|
|
||||||
|
|
||||||
def calculate_left_margin(graph): |
|
||||||
""" |
|
||||||
Calculates the margin to the left of the plot area, setting |
|
||||||
border_left. |
|
||||||
""" |
|
||||||
bl = 7 |
|
||||||
# Check for Y labels |
|
||||||
if graph.rotate_y_labels: |
|
||||||
max_y_label_height_px = graph.y_label_font_size |
|
||||||
else: |
|
||||||
label_lengths = map(len, graph.get_y_labels()) |
|
||||||
max_y_label_len = max(label_lengths) |
|
||||||
max_y_label_height_px = (0.6 * max_y_label_len * |
|
||||||
graph.y_label_font_size) |
|
||||||
if graph.show_y_labels: |
|
||||||
bl += max_y_label_height_px |
|
||||||
if graph.stagger_y_labels: |
|
||||||
bl += max_y_label_height_px + 10 |
|
||||||
if graph.show_y_title: |
|
||||||
bl += graph.y_title_font_size + 5 |
|
||||||
if graph.x_label_rotation: |
|
||||||
first_x_label_width = ( |
|
||||||
graph.x_label_font_size * len( |
|
||||||
(graph.get_x_labels() or [[0]])[0]) * 0.6) |
|
||||||
bl = max(bl, first_x_label_width * cos(graph.x_label_rotation)) |
|
||||||
return bl |
|
||||||
|
|
||||||
|
|
||||||
def calculate_offsets_bottom(graph): |
|
||||||
x_offset = graph.border_left + 20 |
|
||||||
y_offset = graph.border_top + graph.graph_height + 5 |
|
||||||
if graph.show_x_labels: |
|
||||||
max_x_label_height_px = graph.x_label_font_size |
|
||||||
if graph.x_label_rotation: |
|
||||||
longest_label_length = max(map(len, graph.get_x_labels())) |
|
||||||
max_x_label_height_px *= longest_label_length |
|
||||||
max_x_label_height_px *= sin(graph.x_label_rotation) |
|
||||||
y_offset += max_x_label_height_px |
|
||||||
if graph.stagger_x_labels: |
|
||||||
y_offset += max_x_label_height_px + 5 |
|
||||||
if graph.show_x_title: |
|
||||||
y_offset += graph.x_title_font_size + 5 |
|
||||||
return x_offset, y_offset |
|
@ -1,8 +0,0 @@ |
|||||||
[egg_info] |
|
||||||
|
|
||||||
[nosetests] |
|
||||||
with-doctest=1 |
|
||||||
|
|
||||||
[pytest] |
|
||||||
addopts = --doctest-modules |
|
||||||
norecursedirs = build |
|
@ -1,54 +0,0 @@ |
|||||||
#!python |
|
||||||
|
|
||||||
import os |
|
||||||
import sys |
|
||||||
from setuptools import find_packages |
|
||||||
|
|
||||||
from distutils.cmd import Command |
|
||||||
|
|
||||||
|
|
||||||
class DisabledTestCommand(Command): |
|
||||||
user_options = [] |
|
||||||
|
|
||||||
def __init__(self, dist): |
|
||||||
raise RuntimeError( |
|
||||||
"test command not supported on pygal." |
|
||||||
" Use setup.py nosetests instead") |
|
||||||
|
|
||||||
|
|
||||||
setup_params = dict( |
|
||||||
name="pygal", |
|
||||||
description="Python svg graph abstract layer", |
|
||||||
author="Jason R. Coombs, Kozea", |
|
||||||
author_email="jaraco@jaraco.com, gayoub@kozea.fr", |
|
||||||
url="https://github.com/Kozea/pygal", |
|
||||||
packages=find_packages(), |
|
||||||
zip_safe=True, |
|
||||||
include_package_data=True, |
|
||||||
install_requires=[ |
|
||||||
'lxml>=2.0', |
|
||||||
], |
|
||||||
package_data={'pygal': ['css/*']}, |
|
||||||
license="MIT", |
|
||||||
classifiers=[ |
|
||||||
"Development Status :: 5 - Production/Stable", |
|
||||||
"Intended Audience :: Developers", |
|
||||||
"Intended Audience :: Science/Research", |
|
||||||
"Programming Language :: Python :: 2.6", |
|
||||||
"Programming Language :: Python :: 2.7", |
|
||||||
"Programming Language :: Python :: 3", |
|
||||||
"License :: OSI Approved :: MIT License", |
|
||||||
], |
|
||||||
entry_points={ |
|
||||||
}, |
|
||||||
# Don't use setup.py test - nose doesn't support it |
|
||||||
# see http://code.google.com/p/python-nose/issues/detail?id=219 |
|
||||||
cmdclass=dict( |
|
||||||
test=DisabledTestCommand, |
|
||||||
), |
|
||||||
use_2to3=True, |
|
||||||
) |
|
||||||
|
|
||||||
if __name__ == '__main__': |
|
||||||
from setuptools import setup |
|
||||||
setup(**setup_params) |
|
@ -1,119 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
from flask import Flask, Response, render_template, url_for |
|
||||||
from log_colorizer import make_colored_stream_handler |
|
||||||
from logging import getLogger, INFO, WARN, DEBUG |
|
||||||
from moulinrouge.data import labels, series |
|
||||||
from pygal.bar import VerticalBar, HorizontalBar |
|
||||||
from pygal.line import Line |
|
||||||
from pygal.pie import Pie |
|
||||||
import string |
|
||||||
import random |
|
||||||
|
|
||||||
|
|
||||||
def random_label(): |
|
||||||
chars = string.letters + string.digits + u' àéèçêâäëï' |
|
||||||
return ''.join( |
|
||||||
[random.choice(chars) |
|
||||||
for i in range( |
|
||||||
random.randrange(4, 30))]) |
|
||||||
|
|
||||||
|
|
||||||
def random_value(): |
|
||||||
return random.randrange(0, 15, 1) |
|
||||||
|
|
||||||
|
|
||||||
def generate_vbar(**opts): |
|
||||||
g = VerticalBar(labels, opts) |
|
||||||
for serie, values in series.items(): |
|
||||||
g.add_data({'data': values, 'title': serie}) |
|
||||||
|
|
||||||
return Response(g.burn(), mimetype='image/svg+xml') |
|
||||||
|
|
||||||
|
|
||||||
def create_app(): |
|
||||||
"""Creates the pygal test web app""" |
|
||||||
|
|
||||||
app = Flask(__name__) |
|
||||||
handler = make_colored_stream_handler() |
|
||||||
getLogger('werkzeug').addHandler(handler) |
|
||||||
getLogger('werkzeug').setLevel(INFO) |
|
||||||
getLogger('pygal').addHandler(handler) |
|
||||||
getLogger('pygal').setLevel(DEBUG) |
|
||||||
|
|
||||||
@app.route("/") |
|
||||||
def index(): |
|
||||||
return render_template('index.jinja2') |
|
||||||
|
|
||||||
@app.route("/all-<type>.svg") |
|
||||||
def all_svg(type): |
|
||||||
series = random.randrange(1, 10) |
|
||||||
data = random.randrange(1, 10) |
|
||||||
|
|
||||||
labels = [random_label() for i in range(data)] |
|
||||||
|
|
||||||
if type == 'vbar': |
|
||||||
g = VerticalBar(labels) |
|
||||||
elif type == 'hbar': |
|
||||||
g = HorizontalBar(labels) |
|
||||||
elif type == 'pie': |
|
||||||
series = 1 |
|
||||||
g = Pie({'fields': labels}) |
|
||||||
elif type == 'line': |
|
||||||
g = Line({'fields': labels}) |
|
||||||
|
|
||||||
for i in range(series): |
|
||||||
values = [random_value() for i in range(data)] |
|
||||||
g.add_data({'data': values, 'title': random_label()}) |
|
||||||
|
|
||||||
return Response(g.burn(), mimetype='image/svg+xml') |
|
||||||
|
|
||||||
@app.route("/all") |
|
||||||
def all(): |
|
||||||
width, height = 600, 400 |
|
||||||
svgs = [url_for('all_svg', type=type) for type in |
|
||||||
('vbar', 'hbar', 'line', 'pie')] |
|
||||||
return render_template('svgs.jinja2', |
|
||||||
svgs=svgs, |
|
||||||
width=width, |
|
||||||
height=height) |
|
||||||
|
|
||||||
@app.route("/rotation[<int:angle>].svg") |
|
||||||
def rotation_svg(angle): |
|
||||||
return generate_vbar( |
|
||||||
show_graph_title=True, |
|
||||||
graph_title="Rotation %d" % angle, |
|
||||||
x_label_rotation=angle) |
|
||||||
|
|
||||||
@app.route("/rotation") |
|
||||||
def rotation(): |
|
||||||
width, height = 375, 245 |
|
||||||
svgs = [url_for('rotation_svg', angle=angle) |
|
||||||
for angle in range(0, 91, 5)] |
|
||||||
return render_template('svgs.jinja2', |
|
||||||
svgs=svgs, |
|
||||||
width=width, |
|
||||||
height=height) |
|
||||||
|
|
||||||
@app.route("/bigline.svg") |
|
||||||
def big_line_svg(): |
|
||||||
g = Line({'fields': ['a', 'b', 'c', 'd']}) |
|
||||||
g.width = 1500 |
|
||||||
g.area_fill = True |
|
||||||
g.scale_divisions = 50 |
|
||||||
# values = [120, 50, 42, 100] |
|
||||||
# g.add_data({'data': values, 'title': '1'}) |
|
||||||
values = [11, 50, 133, 2] |
|
||||||
g.add_data({'data': values, 'title': '2'}) |
|
||||||
|
|
||||||
return Response(g.burn(), mimetype='image/svg+xml') |
|
||||||
|
|
||||||
@app.route("/bigline") |
|
||||||
def big_line(): |
|
||||||
width, height = 900, 800 |
|
||||||
svgs = [url_for('big_line_svg')] |
|
||||||
return render_template('svgs.jinja2', |
|
||||||
svgs=svgs, |
|
||||||
width=width, |
|
||||||
height=height) |
|
||||||
|
|
||||||
return app |
|
@ -1,11 +0,0 @@ |
|||||||
# -*- coding: utf-8 -*- |
|
||||||
|
|
||||||
labels = ['AURSAUTRAUIA', |
|
||||||
'dpvluiqhu enuie', |
|
||||||
'su sru a nanan a', |
|
||||||
'09_28_3023_98120398', |
|
||||||
u'éàé瀮ð{æə|&'] |
|
||||||
series = { |
|
||||||
'Female': [4, 2, 3, 0, 2], |
|
||||||
'Male': [5, 1, 1, 3, 2] |
|
||||||
} |
|
@ -1,9 +0,0 @@ |
|||||||
html, body, section, figure { |
|
||||||
margin: 0; |
|
||||||
padding: 0; |
|
||||||
} |
|
||||||
|
|
||||||
figure { |
|
||||||
float: left; |
|
||||||
border: 1px solid #ccc; |
|
||||||
} |
|
@ -1,24 +0,0 @@ |
|||||||
$(function () { |
|
||||||
$('figure figcaption').append( |
|
||||||
$('<button>') |
|
||||||
.text('⟳') |
|
||||||
.click(function() { |
|
||||||
var $fig, $embed, w, h, src; |
|
||||||
$fig = $(this).closest('figure'); |
|
||||||
$embed = $fig.find('embed'); |
|
||||||
w = $embed.width(); |
|
||||||
h = $embed.height(); |
|
||||||
src = $embed.attr('src'); |
|
||||||
$embed.remove(); |
|
||||||
$fig.prepend( |
|
||||||
$('<embed>') |
|
||||||
.attr({ |
|
||||||
src: src, |
|
||||||
type: 'image/svg+xml', |
|
||||||
width: w, |
|
||||||
height: h |
|
||||||
}) |
|
||||||
); |
|
||||||
}) |
|
||||||
); |
|
||||||
}); |
|
@ -1,15 +0,0 @@ |
|||||||
<!DOCTYPE html> |
|
||||||
<html> |
|
||||||
<head> |
|
||||||
<title>Moulin rouge - pygal test platform</title> |
|
||||||
<script src="http://code.jquery.com/jquery.min.js" type="text/javascript"></script> |
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js.js') }}"></script> |
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css.css') }}" type="text/css" /> |
|
||||||
</head> |
|
||||||
<body> |
|
||||||
<section> |
|
||||||
{% block section %} |
|
||||||
{% endblock section %} |
|
||||||
</section> |
|
||||||
</body> |
|
||||||
</html> |
|
@ -1,7 +0,0 @@ |
|||||||
{% extends '_layout.jinja2' %} |
|
||||||
|
|
||||||
{% block section %} |
|
||||||
<a href="{{ url_for('all') }}">All types</a> |
|
||||||
<a href="{{ url_for('rotation') }}">Rotations test</a> |
|
||||||
<a href="{{ url_for('big_line') }}">Big line</a> |
|
||||||
{% endblock section %} |
|
@ -1,10 +0,0 @@ |
|||||||
{% extends '_layout.jinja2' %} |
|
||||||
|
|
||||||
{% block section %} |
|
||||||
{% for svg in svgs %} |
|
||||||
<figure> |
|
||||||
<embed src="{{ svg }}" type="image/svg+xml" width="{{ width }}" height="{{ height }}" /> |
|
||||||
<figcaption></figcaption> |
|
||||||
</figure> |
|
||||||
{% endfor %} |
|
||||||
{% endblock section %} |
|
Loading…
Reference in new issue