mirror of https://github.com/Kozea/pygal.git
Florian Mounier
13 years ago
17 changed files with 2750 additions and 2751 deletions
@ -1,8 +1,8 @@
|
||||
#!python |
||||
# -*- coding: utf-8 -*- |
||||
|
||||
""" |
||||
pygal package. |
||||
""" |
||||
|
||||
__all__ = ('graph', 'plot', 'time_series', 'bar', 'pie', 'schedule', 'util') |
||||
#!python |
||||
# -*- coding: utf-8 -*- |
||||
|
||||
""" |
||||
pygal package. |
||||
""" |
||||
|
||||
__all__ = ('graph', 'plot', 'time_series', 'bar', 'pie', 'schedule', 'util') |
||||
|
@ -1,68 +1,68 @@
|
||||
.bar .dataPointLabel { |
||||
fill-opacity: 0; |
||||
-webkit-transition: 250ms; |
||||
} |
||||
|
||||
.bar:hover .dataPointLabel { |
||||
fill-opacity: 0.9; |
||||
fill: #000000; |
||||
} |
||||
|
||||
.key, .fill { |
||||
fill-opacity: 0.5; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
-webkit-transition: 250ms; |
||||
} |
||||
|
||||
.fill:hover { |
||||
fill-opacity: 0.25; |
||||
} |
||||
|
||||
.key1, .fill1 { |
||||
fill: #ff0000; |
||||
} |
||||
|
||||
.key2, .fill2 { |
||||
fill: #0000ff; |
||||
} |
||||
|
||||
.key3, .fill3 { |
||||
fill: #00ff00; |
||||
} |
||||
|
||||
.key4, .fill4 { |
||||
fill: #ffcc00; |
||||
} |
||||
|
||||
.key5, .fill5 { |
||||
fill: #00ccff; |
||||
} |
||||
|
||||
.key6, .fill6 { |
||||
fill: #ff00ff; |
||||
} |
||||
|
||||
.key7, .fill7 { |
||||
fill: #00ffff; |
||||
} |
||||
|
||||
.key8, .fill8 { |
||||
fill: #ffff00; |
||||
} |
||||
|
||||
.key9, .fill9 { |
||||
fill: #cc6666; |
||||
} |
||||
|
||||
.key10, .fill10 { |
||||
fill: #663399; |
||||
} |
||||
|
||||
.key11, .fill11 { |
||||
fill: #339900; |
||||
} |
||||
|
||||
.key12, .fill12 { |
||||
fill: #9966FF; |
||||
} |
||||
.bar .dataPointLabel { |
||||
fill-opacity: 0; |
||||
-webkit-transition: 250ms; |
||||
} |
||||
|
||||
.bar:hover .dataPointLabel { |
||||
fill-opacity: 0.9; |
||||
fill: #000000; |
||||
} |
||||
|
||||
.key, .fill { |
||||
fill-opacity: 0.5; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
-webkit-transition: 250ms; |
||||
} |
||||
|
||||
.fill:hover { |
||||
fill-opacity: 0.25; |
||||
} |
||||
|
||||
.key1, .fill1 { |
||||
fill: #ff0000; |
||||
} |
||||
|
||||
.key2, .fill2 { |
||||
fill: #0000ff; |
||||
} |
||||
|
||||
.key3, .fill3 { |
||||
fill: #00ff00; |
||||
} |
||||
|
||||
.key4, .fill4 { |
||||
fill: #ffcc00; |
||||
} |
||||
|
||||
.key5, .fill5 { |
||||
fill: #00ccff; |
||||
} |
||||
|
||||
.key6, .fill6 { |
||||
fill: #ff00ff; |
||||
} |
||||
|
||||
.key7, .fill7 { |
||||
fill: #00ffff; |
||||
} |
||||
|
||||
.key8, .fill8 { |
||||
fill: #ffff00; |
||||
} |
||||
|
||||
.key9, .fill9 { |
||||
fill: #cc6666; |
||||
} |
||||
|
||||
.key10, .fill10 { |
||||
fill: #663399; |
||||
} |
||||
|
||||
.key11, .fill11 { |
||||
fill: #339900; |
||||
} |
||||
|
||||
.key12, .fill12 { |
||||
fill: #9966FF; |
||||
} |
||||
|
@ -1,82 +1,82 @@
|
||||
import cssutils |
||||
|
||||
SVG = 'SVG 1.1' # http://www.w3.org/TR/SVG11/styling.html |
||||
|
||||
macros = { |
||||
'paint': 'none|currentColor|{color}', |
||||
'unitidentifier': 'em|ex|px|pt|pc|cm|mm|in|%', |
||||
'length': '{positivenum}({unitidentifier})?', |
||||
'dasharray': '{positivenum}(\s*,\s*{positivenum})*', |
||||
# a number greater-than or equal to one |
||||
'number-ge-one': '{[1-9][0-9]*(\.[0-9]+)?', |
||||
} |
||||
properties = { |
||||
# Clipping, Masking, and Compositing |
||||
'clip-path': '{uri}|none|inherit', |
||||
'clip-rule': 'nonzero|evenodd|inherit', |
||||
'mask': '{uri}|none|inherit', |
||||
'opacity': '{num}|inherit', |
||||
|
||||
# Filter Effects |
||||
'enable-background': 'accumulate|new(\s+{num}){0,4}|inherit', |
||||
'filter': '{uri}|none|inherit', |
||||
'flood-color': 'currentColor|{color}|inherit', |
||||
'flood-opacity': '{num}|inherit', |
||||
'lighting-color': 'currentColor|{color}|inherit', |
||||
|
||||
# Gradient Properties |
||||
'stop-color': 'currentColor|{color}|inherit', |
||||
'stop-opacity': '{num}|inherit', |
||||
|
||||
# Interactivity Properties |
||||
'pointer-events': ('visiblePainted|visibleFill|visibleStroke' |
||||
'|visible|painted|fill|stroke|all|none|inherit'), |
||||
|
||||
# Color and Pointing Properties |
||||
'color-interpolation': 'auto|sRGB|linearRGB|inherit', |
||||
'color-interpolation-filters': 'auto|sRGB|linearRGB|inherit', |
||||
'color-rendering': 'auto|optimizeSpeed|optimizeQuality|inherit', |
||||
'shape-rendering': ('auto|optimizeSpeed|crispEdges|' |
||||
'geometricPrecision|inherit'), |
||||
'text-rendering': ('auto|optimizeSpeed|optimizeLegibility' |
||||
'|geometricPrecision|inherit'), |
||||
'fill': '{paint}', |
||||
'fill-opacity': '{num}|inherit', |
||||
'fill-rule': 'nonzero|evenodd|inherit', |
||||
'image-rendering': 'auto|optimizeSpeed|optimizeQuality|inherit', |
||||
'marker': 'none|inherit|{uri}', |
||||
'marker-end': 'none|inherit|{uri}', |
||||
'marker-mid': 'none|inherit|{uri}', |
||||
'marker-start': 'none|inherit|{uri}', |
||||
'shape-rendering': ('auto|optimizeSpeed|crispEdges|' |
||||
'geometricPrecision|inherit'), |
||||
'stroke': '{paint}', |
||||
'stroke-dasharray': 'none|{dasharray}|inherit', |
||||
'stroke-dashoffset': '{length}|inherit', |
||||
'stroke-linecap': 'butt|round|square|inherit', |
||||
'stroke-linejoin': 'miter|round|bevel|inherit', |
||||
'stroke-miterlimit': '{number-ge-one}|inherit', |
||||
'stroke-opacity': '{num}|inherit', |
||||
'stroke-width': '{length}|inherit', |
||||
'text-rendering': ('auto|optimizeSpeed|optimizeLegibility' |
||||
'|geometricPrecision|inherit'), |
||||
|
||||
# Text Properties |
||||
'alignment-baseline': ('auto|baseline|before-edge|text-before-edge|' |
||||
'middle|central|after-edge|text-after-edge|' |
||||
'ideographic|alphabetic|hanging|mathematical' |
||||
'|inherit'), |
||||
'baseline-shift': 'baseline|sub|super|{percentage}|{length}|inherit', |
||||
'dominant-baseline': ('auto|use-script|no-change|reset-size|ideographic|' |
||||
'alphabetic|hanging||mathematical|central|middle|' |
||||
'text-after-edge|text-before-edge|inherit'), |
||||
'glyph-orientation-horizontal': '{angle}|inherit', |
||||
'glyph-orientation-vertical': 'auto|{angle}|inherit', |
||||
'kerning': 'auto|{length}|inherit', |
||||
'text-anchor': 'start|middle|end|inherit', |
||||
'writing-mode': 'lr-tb|rl-tb|tb-rl|lr|rl|tb|inherit', |
||||
} |
||||
|
||||
cssutils.profile.addProfile(SVG, properties, macros) |
||||
|
||||
cssutils.profile.defaultProfiles = [SVG, cssutils.profile.CSS_LEVEL_2] |
||||
import cssutils |
||||
|
||||
SVG = 'SVG 1.1' # http://www.w3.org/TR/SVG11/styling.html |
||||
|
||||
macros = { |
||||
'paint': 'none|currentColor|{color}', |
||||
'unitidentifier': 'em|ex|px|pt|pc|cm|mm|in|%', |
||||
'length': '{positivenum}({unitidentifier})?', |
||||
'dasharray': '{positivenum}(\s*,\s*{positivenum})*', |
||||
# a number greater-than or equal to one |
||||
'number-ge-one': '{[1-9][0-9]*(\.[0-9]+)?', |
||||
} |
||||
properties = { |
||||
# Clipping, Masking, and Compositing |
||||
'clip-path': '{uri}|none|inherit', |
||||
'clip-rule': 'nonzero|evenodd|inherit', |
||||
'mask': '{uri}|none|inherit', |
||||
'opacity': '{num}|inherit', |
||||
|
||||
# Filter Effects |
||||
'enable-background': 'accumulate|new(\s+{num}){0,4}|inherit', |
||||
'filter': '{uri}|none|inherit', |
||||
'flood-color': 'currentColor|{color}|inherit', |
||||
'flood-opacity': '{num}|inherit', |
||||
'lighting-color': 'currentColor|{color}|inherit', |
||||
|
||||
# Gradient Properties |
||||
'stop-color': 'currentColor|{color}|inherit', |
||||
'stop-opacity': '{num}|inherit', |
||||
|
||||
# Interactivity Properties |
||||
'pointer-events': ('visiblePainted|visibleFill|visibleStroke' |
||||
'|visible|painted|fill|stroke|all|none|inherit'), |
||||
|
||||
# Color and Pointing Properties |
||||
'color-interpolation': 'auto|sRGB|linearRGB|inherit', |
||||
'color-interpolation-filters': 'auto|sRGB|linearRGB|inherit', |
||||
'color-rendering': 'auto|optimizeSpeed|optimizeQuality|inherit', |
||||
'shape-rendering': ('auto|optimizeSpeed|crispEdges|' |
||||
'geometricPrecision|inherit'), |
||||
'text-rendering': ('auto|optimizeSpeed|optimizeLegibility' |
||||
'|geometricPrecision|inherit'), |
||||
'fill': '{paint}', |
||||
'fill-opacity': '{num}|inherit', |
||||
'fill-rule': 'nonzero|evenodd|inherit', |
||||
'image-rendering': 'auto|optimizeSpeed|optimizeQuality|inherit', |
||||
'marker': 'none|inherit|{uri}', |
||||
'marker-end': 'none|inherit|{uri}', |
||||
'marker-mid': 'none|inherit|{uri}', |
||||
'marker-start': 'none|inherit|{uri}', |
||||
'shape-rendering': ('auto|optimizeSpeed|crispEdges|' |
||||
'geometricPrecision|inherit'), |
||||
'stroke': '{paint}', |
||||
'stroke-dasharray': 'none|{dasharray}|inherit', |
||||
'stroke-dashoffset': '{length}|inherit', |
||||
'stroke-linecap': 'butt|round|square|inherit', |
||||
'stroke-linejoin': 'miter|round|bevel|inherit', |
||||
'stroke-miterlimit': '{number-ge-one}|inherit', |
||||
'stroke-opacity': '{num}|inherit', |
||||
'stroke-width': '{length}|inherit', |
||||
'text-rendering': ('auto|optimizeSpeed|optimizeLegibility' |
||||
'|geometricPrecision|inherit'), |
||||
|
||||
# Text Properties |
||||
'alignment-baseline': ('auto|baseline|before-edge|text-before-edge|' |
||||
'middle|central|after-edge|text-after-edge|' |
||||
'ideographic|alphabetic|hanging|mathematical' |
||||
'|inherit'), |
||||
'baseline-shift': 'baseline|sub|super|{percentage}|{length}|inherit', |
||||
'dominant-baseline': ('auto|use-script|no-change|reset-size|ideographic|' |
||||
'alphabetic|hanging||mathematical|central|middle|' |
||||
'text-after-edge|text-before-edge|inherit'), |
||||
'glyph-orientation-horizontal': '{angle}|inherit', |
||||
'glyph-orientation-vertical': 'auto|{angle}|inherit', |
||||
'kerning': 'auto|{length}|inherit', |
||||
'text-anchor': 'start|middle|end|inherit', |
||||
'writing-mode': 'lr-tb|rl-tb|tb-rl|lr|rl|tb|inherit', |
||||
} |
||||
|
||||
cssutils.profile.addProfile(SVG, properties, macros) |
||||
|
||||
cssutils.profile.defaultProfiles = [SVG, cssutils.profile.CSS_LEVEL_2] |
||||
|
@ -1,87 +1,87 @@
|
||||
/* |
||||
$Id$ |
||||
|
||||
Base styles for pygal.Graph |
||||
*/ |
||||
|
||||
* { |
||||
font-family: monospace; |
||||
} |
||||
.svgBackground{ |
||||
fill:#ffffff; |
||||
} |
||||
.graphBackground{ |
||||
fill:#ffffff; |
||||
} |
||||
|
||||
/* 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: #eee; |
||||
stroke-width: 1px; |
||||
stroke-dasharray: 5,5; |
||||
} |
||||
|
||||
.xAxisLabels{ |
||||
text-anchor: middle; |
||||
fill: #000000; |
||||
font-size: %(x_label_font_size)dpx; |
||||
font-weight: normal; |
||||
} |
||||
|
||||
.yAxisLabels{ |
||||
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; |
||||
} |
||||
/* |
||||
$Id$ |
||||
|
||||
Base styles for pygal.Graph |
||||
*/ |
||||
|
||||
* { |
||||
font-family: monospace; |
||||
} |
||||
.svgBackground{ |
||||
fill:#ffffff; |
||||
} |
||||
.graphBackground{ |
||||
fill:#ffffff; |
||||
} |
||||
|
||||
/* 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: #eee; |
||||
stroke-width: 1px; |
||||
stroke-dasharray: 5,5; |
||||
} |
||||
|
||||
.xAxisLabels{ |
||||
text-anchor: middle; |
||||
fill: #000000; |
||||
font-size: %(x_label_font_size)dpx; |
||||
font-weight: normal; |
||||
} |
||||
|
||||
.yAxisLabels{ |
||||
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; |
||||
} |
||||
|
@ -1,178 +1,178 @@
|
||||
#!python |
||||
|
||||
# $Id$ |
||||
|
||||
from operator import itemgetter, add |
||||
from lxml import etree |
||||
|
||||
from util import 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) |
||||
|
||||
def get_y_label_values(self): |
||||
max_value = self.max_value() |
||||
min_value = self.min_value() |
||||
range = max_value - min_value |
||||
top_pad = (range / 20.0) or 10 |
||||
scale_range = (max_value + top_pad) - min_value |
||||
|
||||
scale_division = self.scale_divisions or (scale_range / 10.0) |
||||
|
||||
if self.scale_integers: |
||||
scale_division = min(1, round(scale_division)) |
||||
|
||||
if max_value % scale_division == 0: |
||||
max_value += scale_division |
||||
labels = tuple(float_range(min_value, max_value, scale_division)) |
||||
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): |
||||
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) |
||||
|
||||
field_width = self.field_width() |
||||
#line = len(self.data) |
||||
|
||||
prev_sum = [0] * len(self.fields) |
||||
cum_sum = [-min_value] * len(self.fields) |
||||
|
||||
coord_format = lambda c: '%(x)s %(y)s' % c |
||||
|
||||
for line_n, data in reversed(list(enumerate(self.data, 1))): |
||||
apath = '' |
||||
|
||||
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) |
||||
|
||||
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%(graph_height)s" % vars(self) |
||||
origin = coord_format(get_coords((0, 0))) |
||||
|
||||
d = ' '.join(( |
||||
'M', |
||||
origin, |
||||
'L', |
||||
line_path, |
||||
area_path, |
||||
'Z' |
||||
)) |
||||
etree.SubElement(self.graph, 'path', { |
||||
'class': 'fill%(line_n)s' % vars(), |
||||
'd': d, |
||||
}) |
||||
|
||||
# now draw the line itself |
||||
etree.SubElement(self.graph, 'path', { |
||||
'd': 'M0 %s L%s' % (self.graph_height, line_path), |
||||
'class': 'line%(line_n)s' % vars(), |
||||
}) |
||||
|
||||
if self.show_data_points or self.show_data_values: |
||||
for i, value in enumerate(cum_sum): |
||||
if self.show_data_points: |
||||
circle = etree.SubElement( |
||||
self.graph, |
||||
'circle', |
||||
{'class': 'dataPoint%(line_n)s' % vars()}, |
||||
cx=str(field_width * i), |
||||
cy=str(self.graph_height - value * field_height), |
||||
r='2.5', |
||||
) |
||||
self.make_datapoint_text( |
||||
field_width * i, |
||||
self.graph_height - value * field_height - 6, |
||||
value + min_value |
||||
) |
||||
|
||||
prev_sum = list(cum_sum) |
||||
#!python |
||||
|
||||
# $Id$ |
||||
|
||||
from operator import itemgetter, add |
||||
from lxml import etree |
||||
|
||||
from util import 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) |
||||
|
||||
def get_y_label_values(self): |
||||
max_value = self.max_value() |
||||
min_value = self.min_value() |
||||
range = max_value - min_value |
||||
top_pad = (range / 20.0) or 10 |
||||
scale_range = (max_value + top_pad) - min_value |
||||
|
||||
scale_division = self.scale_divisions or (scale_range / 10.0) |
||||
|
||||
if self.scale_integers: |
||||
scale_division = min(1, round(scale_division)) |
||||
|
||||
if max_value % scale_division == 0: |
||||
max_value += scale_division |
||||
labels = tuple(float_range(min_value, max_value, scale_division)) |
||||
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): |
||||
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) |
||||
|
||||
field_width = self.field_width() |
||||
#line = len(self.data) |
||||
|
||||
prev_sum = [0] * len(self.fields) |
||||
cum_sum = [-min_value] * len(self.fields) |
||||
|
||||
coord_format = lambda c: '%(x)s %(y)s' % c |
||||
|
||||
for line_n, data in reversed(list(enumerate(self.data, 1))): |
||||
apath = '' |
||||
|
||||
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) |
||||
|
||||
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%(graph_height)s" % vars(self) |
||||
origin = coord_format(get_coords((0, 0))) |
||||
|
||||
d = ' '.join(( |
||||
'M', |
||||
origin, |
||||
'L', |
||||
line_path, |
||||
area_path, |
||||
'Z' |
||||
)) |
||||
etree.SubElement(self.graph, 'path', { |
||||
'class': 'fill%(line_n)s' % vars(), |
||||
'd': d, |
||||
}) |
||||
|
||||
# now draw the line itself |
||||
etree.SubElement(self.graph, 'path', { |
||||
'd': 'M0 %s L%s' % (self.graph_height, line_path), |
||||
'class': 'line%(line_n)s' % vars(), |
||||
}) |
||||
|
||||
if self.show_data_points or self.show_data_values: |
||||
for i, value in enumerate(cum_sum): |
||||
if self.show_data_points: |
||||
circle = etree.SubElement( |
||||
self.graph, |
||||
'circle', |
||||
{'class': 'dataPoint%(line_n)s' % vars()}, |
||||
cx=str(field_width * i), |
||||
cy=str(self.graph_height - value * field_height), |
||||
r='2.5', |
||||
) |
||||
self.make_datapoint_text( |
||||
field_width * i, |
||||
self.graph_height - value * field_height - 6, |
||||
value + min_value |
||||
) |
||||
|
||||
prev_sum = list(cum_sum) |
||||
|
@ -1,81 +1,81 @@
|
||||
.dataPointLabel{ |
||||
fill: #000000; |
||||
text-anchor:middle; |
||||
font-size: %(datapoint_font_size)spx; |
||||
font-family: "Arial", sans-serif; |
||||
font-weight: normal; |
||||
} |
||||
|
||||
/* key - MUST match fill styles */ |
||||
.key1,.fill1{ |
||||
fill: #ff0000; |
||||
fill-opacity: 0.7; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key2,.fill2{ |
||||
fill: #0000ff; |
||||
fill-opacity: 0.7; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key3,.fill3{ |
||||
fill-opacity: 0.7; |
||||
fill: #00ff00; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key4,.fill4{ |
||||
fill-opacity: 0.7; |
||||
fill: #ffcc00; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key5,.fill5{ |
||||
fill-opacity: 0.7; |
||||
fill: #00ccff; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key6,.fill6{ |
||||
fill-opacity: 0.7; |
||||
fill: #ff00ff; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key7,.fill7{ |
||||
fill-opacity: 0.7; |
||||
fill: #00ff99; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key8,.fill8{ |
||||
fill-opacity: 0.7; |
||||
fill: #ffff00; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key9,.fill9{ |
||||
fill-opacity: 0.7; |
||||
fill: #cc6666; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key10,.fill10{ |
||||
fill-opacity: 0.7; |
||||
fill: #663399; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key11,.fill11{ |
||||
fill-opacity: 0.7; |
||||
fill: #339900; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key12,.fill12{ |
||||
fill-opacity: 0.7; |
||||
fill: #9966FF; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.dataPointLabel{ |
||||
fill: #000000; |
||||
text-anchor:middle; |
||||
font-size: %(datapoint_font_size)spx; |
||||
font-family: "Arial", sans-serif; |
||||
font-weight: normal; |
||||
} |
||||
|
||||
/* key - MUST match fill styles */ |
||||
.key1,.fill1{ |
||||
fill: #ff0000; |
||||
fill-opacity: 0.7; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key2,.fill2{ |
||||
fill: #0000ff; |
||||
fill-opacity: 0.7; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key3,.fill3{ |
||||
fill-opacity: 0.7; |
||||
fill: #00ff00; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key4,.fill4{ |
||||
fill-opacity: 0.7; |
||||
fill: #ffcc00; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key5,.fill5{ |
||||
fill-opacity: 0.7; |
||||
fill: #00ccff; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key6,.fill6{ |
||||
fill-opacity: 0.7; |
||||
fill: #ff00ff; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key7,.fill7{ |
||||
fill-opacity: 0.7; |
||||
fill: #00ff99; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key8,.fill8{ |
||||
fill-opacity: 0.7; |
||||
fill: #ffff00; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key9,.fill9{ |
||||
fill-opacity: 0.7; |
||||
fill: #cc6666; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key10,.fill10{ |
||||
fill-opacity: 0.7; |
||||
fill: #663399; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key11,.fill11{ |
||||
fill-opacity: 0.7; |
||||
fill: #339900; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key12,.fill12{ |
||||
fill-opacity: 0.7; |
||||
fill: #9966FF; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
|
@ -1,302 +1,302 @@
|
||||
import math |
||||
import itertools |
||||
from lxml import etree |
||||
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 = True |
||||
"Sets the offset of the shadow from the pie chart" |
||||
shadow_offset = 10 |
||||
|
||||
show_data_labels = False |
||||
"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" |
||||
etree.SubElement( |
||||
defs, |
||||
'filter', |
||||
id='dropshadow', |
||||
width='1.2', |
||||
height='1.2', |
||||
) |
||||
etree.SubElement( |
||||
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) |
||||
percent_scale = 100.0 / total |
||||
|
||||
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 = etree.SubElement(self.root, 'g') |
||||
background = etree.SubElement(self.graph, 'g') |
||||
# midground is somewhere between the background and the foreground |
||||
midground = etree.SubElement(self.graph, 'g') |
||||
|
||||
is_expanded = (self.expanded or self.expand_greatest) |
||||
diameter = min(self.graph_width, self.graph_height) |
||||
# 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(%(xoff)s %(yoff)s)' % vars() |
||||
self.graph.set('transform', transform) |
||||
|
||||
wedge_text_pad = 5 |
||||
wedge_text_pad = (20 * int(self.show_percent) * |
||||
int(self.show_data_labels)) |
||||
|
||||
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 = ' '.join(( |
||||
"M%(radius)s,%(radius)s", |
||||
"L%(x_start)s,%(y_start)s", |
||||
"A%(radius)s,%(radius)s", |
||||
"0,", |
||||
"%(percent_greater_fifty)s,1,", |
||||
"%(x_end)s %(y_end)s Z")) |
||||
path = path % vars() |
||||
|
||||
wedge = etree.SubElement( |
||||
self.foreground, |
||||
'path', |
||||
{ |
||||
'd': path, |
||||
'class': '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 = etree.SubElement( |
||||
background, |
||||
'path', |
||||
d=path, |
||||
filter='url(#dropshadow)', |
||||
style='fill: #ccc; stroke: none', |
||||
) |
||||
clear = etree.SubElement( |
||||
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(%(tx)s %(ty)s)" % vars() |
||||
wedge.set('transform', translate) |
||||
clear.set('transform', translate) |
||||
|
||||
if self.show_shadow: |
||||
shadow_tx = self.shadow_offset + tx |
||||
shadow_ty = self.shadow_offset + ty |
||||
translate = 'translate(%(shadow_tx)s %(shadow_ty)s)' % vars() |
||||
shadow.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 = etree.SubElement( |
||||
self.foreground, |
||||
'text', |
||||
{ |
||||
'x': str(tx), |
||||
'y': str(ty), |
||||
'class': 'dataPointLabel', |
||||
'style': 'stroke: #fff; stroke-width: 2;' |
||||
} |
||||
) |
||||
label_node.text = label |
||||
|
||||
label_node = etree.SubElement( |
||||
self.foreground, |
||||
'text', |
||||
{ |
||||
'x': str(tx), |
||||
'y': str(ty), |
||||
'class': 'dataPointLabel', |
||||
} |
||||
) |
||||
label_node.text = label |
||||
|
||||
prev_percent += percent |
||||
|
||||
def round(self, val, to): |
||||
return round(val, to) |
||||
import math |
||||
import itertools |
||||
from lxml import etree |
||||
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 = True |
||||
"Sets the offset of the shadow from the pie chart" |
||||
shadow_offset = 10 |
||||
|
||||
show_data_labels = False |
||||
"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" |
||||
etree.SubElement( |
||||
defs, |
||||
'filter', |
||||
id='dropshadow', |
||||
width='1.2', |
||||
height='1.2', |
||||
) |
||||
etree.SubElement( |
||||
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) |
||||
percent_scale = 100.0 / total |
||||
|
||||
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 = etree.SubElement(self.root, 'g') |
||||
background = etree.SubElement(self.graph, 'g') |
||||
# midground is somewhere between the background and the foreground |
||||
midground = etree.SubElement(self.graph, 'g') |
||||
|
||||
is_expanded = (self.expanded or self.expand_greatest) |
||||
diameter = min(self.graph_width, self.graph_height) |
||||
# 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(%(xoff)s %(yoff)s)' % vars() |
||||
self.graph.set('transform', transform) |
||||
|
||||
wedge_text_pad = 5 |
||||
wedge_text_pad = (20 * int(self.show_percent) * |
||||
int(self.show_data_labels)) |
||||
|
||||
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 = ' '.join(( |
||||
"M%(radius)s,%(radius)s", |
||||
"L%(x_start)s,%(y_start)s", |
||||
"A%(radius)s,%(radius)s", |
||||
"0,", |
||||
"%(percent_greater_fifty)s,1,", |
||||
"%(x_end)s %(y_end)s Z")) |
||||
path = path % vars() |
||||
|
||||
wedge = etree.SubElement( |
||||
self.foreground, |
||||
'path', |
||||
{ |
||||
'd': path, |
||||
'class': '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 = etree.SubElement( |
||||
background, |
||||
'path', |
||||
d=path, |
||||
filter='url(#dropshadow)', |
||||
style='fill: #ccc; stroke: none', |
||||
) |
||||
clear = etree.SubElement( |
||||
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(%(tx)s %(ty)s)" % vars() |
||||
wedge.set('transform', translate) |
||||
clear.set('transform', translate) |
||||
|
||||
if self.show_shadow: |
||||
shadow_tx = self.shadow_offset + tx |
||||
shadow_ty = self.shadow_offset + ty |
||||
translate = 'translate(%(shadow_tx)s %(shadow_ty)s)' % vars() |
||||
shadow.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 = etree.SubElement( |
||||
self.foreground, |
||||
'text', |
||||
{ |
||||
'x': str(tx), |
||||
'y': str(ty), |
||||
'class': 'dataPointLabel', |
||||
'style': 'stroke: #fff; stroke-width: 2;' |
||||
} |
||||
) |
||||
label_node.text = label |
||||
|
||||
label_node = etree.SubElement( |
||||
self.foreground, |
||||
'text', |
||||
{ |
||||
'x': str(tx), |
||||
'y': str(ty), |
||||
'class': 'dataPointLabel', |
||||
} |
||||
) |
||||
label_node.text = label |
||||
|
||||
prev_percent += percent |
||||
|
||||
def round(self, val, to): |
||||
return round(val, to) |
||||
|
@ -1,193 +1,193 @@
|
||||
/* |
||||
$Id$ |
||||
|
||||
default line styles |
||||
*/ |
||||
.line1{ |
||||
fill: none; |
||||
stroke: #ff0000; |
||||
stroke-width: 1px; |
||||
} |
||||
.line2{ |
||||
fill: none; |
||||
stroke: #0000ff; |
||||
stroke-width: 1px; |
||||
} |
||||
.line3{ |
||||
fill: none; |
||||
stroke: #00ff00; |
||||
stroke-width: 1px; |
||||
} |
||||
.line4{ |
||||
fill: none; |
||||
stroke: #ffcc00; |
||||
stroke-width: 1px; |
||||
} |
||||
.line5{ |
||||
fill: none; |
||||
stroke: #00ccff; |
||||
stroke-width: 1px; |
||||
} |
||||
.line6{ |
||||
fill: none; |
||||
stroke: #ff00ff; |
||||
stroke-width: 1px; |
||||
} |
||||
.line7{ |
||||
fill: none; |
||||
stroke: #00ffff; |
||||
stroke-width: 1px; |
||||
} |
||||
.line8{ |
||||
fill: none; |
||||
stroke: #ffff00; |
||||
stroke-width: 1px; |
||||
} |
||||
.line9{ |
||||
fill: none; |
||||
stroke: #cc6666; |
||||
stroke-width: 1px; |
||||
} |
||||
.line10{ |
||||
fill: none; |
||||
stroke: #663399; |
||||
stroke-width: 1px; |
||||
} |
||||
.line11{ |
||||
fill: none; |
||||
stroke: #339900; |
||||
stroke-width: 1px; |
||||
} |
||||
.line12{ |
||||
fill: none; |
||||
stroke: #9966FF; |
||||
stroke-width: 1px; |
||||
} |
||||
/* default fill styles */ |
||||
.fill1{ |
||||
fill: #cc0000; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill2{ |
||||
fill: #0000cc; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill3{ |
||||
fill: #00cc00; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill4{ |
||||
fill: #ffcc00; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill5{ |
||||
fill: #00ccff; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill6{ |
||||
fill: #ff00ff; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill7{ |
||||
fill: #00ffff; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill8{ |
||||
fill: #ffff00; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill9{ |
||||
fill: #cc6666; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill10{ |
||||
fill: #663399; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill11{ |
||||
fill: #339900; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill12{ |
||||
fill: #9966FF; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
/* default line styles */ |
||||
.key1,.dataPoint1{ |
||||
fill: #ff0000; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key2,.dataPoint2{ |
||||
fill: #0000ff; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key3,.dataPoint3{ |
||||
fill: #00ff00; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key4,.dataPoint4{ |
||||
fill: #ffcc00; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key5,.dataPoint5{ |
||||
fill: #00ccff; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key6,.dataPoint6{ |
||||
fill: #ff00ff; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key7,.dataPoint7{ |
||||
fill: #00ffff; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key8,.dataPoint8{ |
||||
fill: #ffff00; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key9,.dataPoint9{ |
||||
fill: #cc6666; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key10,.dataPoint10{ |
||||
fill: #663399; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key11,.dataPoint11{ |
||||
fill: #339900; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key12,.dataPoint12{ |
||||
fill: #9966FF; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.constantLine{ |
||||
color: navy; |
||||
stroke: navy; |
||||
stroke-width: 1px; |
||||
stroke-dasharray: 9,1,1; |
||||
} |
||||
/* |
||||
$Id$ |
||||
|
||||
default line styles |
||||
*/ |
||||
.line1{ |
||||
fill: none; |
||||
stroke: #ff0000; |
||||
stroke-width: 1px; |
||||
} |
||||
.line2{ |
||||
fill: none; |
||||
stroke: #0000ff; |
||||
stroke-width: 1px; |
||||
} |
||||
.line3{ |
||||
fill: none; |
||||
stroke: #00ff00; |
||||
stroke-width: 1px; |
||||
} |
||||
.line4{ |
||||
fill: none; |
||||
stroke: #ffcc00; |
||||
stroke-width: 1px; |
||||
} |
||||
.line5{ |
||||
fill: none; |
||||
stroke: #00ccff; |
||||
stroke-width: 1px; |
||||
} |
||||
.line6{ |
||||
fill: none; |
||||
stroke: #ff00ff; |
||||
stroke-width: 1px; |
||||
} |
||||
.line7{ |
||||
fill: none; |
||||
stroke: #00ffff; |
||||
stroke-width: 1px; |
||||
} |
||||
.line8{ |
||||
fill: none; |
||||
stroke: #ffff00; |
||||
stroke-width: 1px; |
||||
} |
||||
.line9{ |
||||
fill: none; |
||||
stroke: #cc6666; |
||||
stroke-width: 1px; |
||||
} |
||||
.line10{ |
||||
fill: none; |
||||
stroke: #663399; |
||||
stroke-width: 1px; |
||||
} |
||||
.line11{ |
||||
fill: none; |
||||
stroke: #339900; |
||||
stroke-width: 1px; |
||||
} |
||||
.line12{ |
||||
fill: none; |
||||
stroke: #9966FF; |
||||
stroke-width: 1px; |
||||
} |
||||
/* default fill styles */ |
||||
.fill1{ |
||||
fill: #cc0000; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill2{ |
||||
fill: #0000cc; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill3{ |
||||
fill: #00cc00; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill4{ |
||||
fill: #ffcc00; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill5{ |
||||
fill: #00ccff; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill6{ |
||||
fill: #ff00ff; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill7{ |
||||
fill: #00ffff; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill8{ |
||||
fill: #ffff00; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill9{ |
||||
fill: #cc6666; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill10{ |
||||
fill: #663399; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill11{ |
||||
fill: #339900; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
.fill12{ |
||||
fill: #9966FF; |
||||
fill-opacity: 0.2; |
||||
stroke: none; |
||||
} |
||||
/* default line styles */ |
||||
.key1,.dataPoint1{ |
||||
fill: #ff0000; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key2,.dataPoint2{ |
||||
fill: #0000ff; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key3,.dataPoint3{ |
||||
fill: #00ff00; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key4,.dataPoint4{ |
||||
fill: #ffcc00; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key5,.dataPoint5{ |
||||
fill: #00ccff; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key6,.dataPoint6{ |
||||
fill: #ff00ff; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key7,.dataPoint7{ |
||||
fill: #00ffff; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key8,.dataPoint8{ |
||||
fill: #ffff00; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key9,.dataPoint9{ |
||||
fill: #cc6666; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key10,.dataPoint10{ |
||||
fill: #663399; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key11,.dataPoint11{ |
||||
fill: #339900; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.key12,.dataPoint12{ |
||||
fill: #9966FF; |
||||
stroke: none; |
||||
stroke-width: 1px; |
||||
} |
||||
.constantLine{ |
||||
color: navy; |
||||
stroke: navy; |
||||
stroke-width: 1px; |
||||
stroke-dasharray: 9,1,1; |
||||
} |
||||
|
@ -1,363 +1,363 @@
|
||||
# -*- coding: utf-8 -*- |
||||
|
||||
"plot.py" |
||||
|
||||
import sys |
||||
from itertools import izip, count, chain |
||||
from lxml import etree |
||||
|
||||
from pygal.graph import Graph |
||||
|
||||
from .util import float_range |
||||
|
||||
|
||||
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): |
||||
"""=== For creating SVG plots of scalar data |
||||
|
||||
= Synopsis |
||||
|
||||
require 'SVG/Graph/Plot' |
||||
|
||||
# Data sets are x,y pairs |
||||
# Note that multiple data sets can differ in length, and that the |
||||
# data in the datasets needn't be in order; they will be ordered |
||||
# by the plot along the X-axis. |
||||
projection = [ |
||||
6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13, |
||||
7, 9 |
||||
] |
||||
actual = [ |
||||
0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12, |
||||
15, 6, 4, 17, 2, 12 |
||||
] |
||||
|
||||
graph = SVG::Graph::Plot.new({ |
||||
:height => 500, |
||||
:width => 300, |
||||
:key => true, |
||||
:scale_x_integers => true, |
||||
:scale_y_integerrs => true, |
||||
}) |
||||
|
||||
graph.add_data({ |
||||
:data => projection |
||||
:title => 'Projected', |
||||
}) |
||||
|
||||
graph.add_data({ |
||||
:data => actual, |
||||
:title => 'Actual', |
||||
}) |
||||
|
||||
print graph.burn() |
||||
|
||||
= Description |
||||
|
||||
Produces a graph of scalar data. |
||||
|
||||
This object aims to allow you to easily create high quality |
||||
SVG[http://www.w3c.org/tr/svg] scalar plots. You can either use the |
||||
default style sheet or supply your own. Either way there are many options |
||||
which can be configured to give you control over how the graph is |
||||
generated - with or without a key, data elements at each point, title, |
||||
subtitle etc. |
||||
|
||||
= Examples |
||||
|
||||
http://www.germane-software/repositories/public/SVG/test/plot.rb |
||||
|
||||
= Notes |
||||
|
||||
The default stylesheet handles upto 10 data sets, if you |
||||
use more you must create your own stylesheet and add the |
||||
additional settings for the extra data sets. You will know |
||||
if you go over 10 data sets as they will have no style and |
||||
be in black. |
||||
|
||||
Unlike the other types of charts, data sets must contain x,y pairs: |
||||
|
||||
[1, 2] # A data set with 1 point: (1,2) |
||||
[1,2, 5,6] # A data set with 2 points: (1,2) and (5,6) |
||||
|
||||
= See also |
||||
|
||||
* SVG::Graph::Graph |
||||
* SVG::Graph::BarHorizontal |
||||
* SVG::Graph::Bar |
||||
* SVG::Graph::Line |
||||
* SVG::Graph::Pie |
||||
* SVG::Graph::TimeSeries |
||||
|
||||
== Author |
||||
|
||||
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> |
||||
|
||||
Copyright 2004 Sean E. Russell |
||||
This software is available under the Ruby license[LICENSE.txt]""" |
||||
|
||||
top_align = right_align = top_font = right_font = 1 |
||||
|
||||
"""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(): |
||||
doc = """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 |
||||
path = etree.SubElement(self.graph, 'path', { |
||||
'd': 'M%(x_start)f %(graph_height)f' |
||||
' %(lpath)s V%(graph_height)f Z' % vars(), |
||||
'class': 'fill%(line)d' % vars()}) |
||||
if self.draw_lines_between_points: |
||||
path = etree.SubElement(self.graph, 'path', { |
||||
'd': 'M%(x_start)f %(y_start)f %(lpath)s' % vars(), |
||||
'class': 'line%(line)d' % vars()}) |
||||
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 = etree.SubElement(self.graph, 'path', { |
||||
'd': 'M 0 %(start)s h%(stop)s' % vars(), |
||||
'class': 'constantLine'}) |
||||
if style: |
||||
path.set('style', style) |
||||
text = etree.SubElement(self.graph, 'text', { |
||||
'x': str(2), |
||||
'y': str(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'] |
||||
#locals().update(self.__transform_parameters) |
||||
#vars().update(self.__transform_parameters) |
||||
x = (x - x_min) * x_step |
||||
y = self.graph_height - (y - y_min) * y_step |
||||
return x, y |
||||
|
||||
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: |
||||
etree.SubElement(self.graph, 'circle', { |
||||
'cx': str(gx), |
||||
'cy': str(gy), |
||||
'r': '2.5', |
||||
'class': 'dataPoint%(line)s' % vars()}) |
||||
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) |
||||
# -*- coding: utf-8 -*- |
||||
|
||||
"plot.py" |
||||
|
||||
import sys |
||||
from itertools import izip, count, chain |
||||
from lxml import etree |
||||
|
||||
from pygal.graph import Graph |
||||
|
||||
from .util import float_range |
||||
|
||||
|
||||
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): |
||||
"""=== For creating SVG plots of scalar data |
||||
|
||||
= Synopsis |
||||
|
||||
require 'SVG/Graph/Plot' |
||||
|
||||
# Data sets are x,y pairs |
||||
# Note that multiple data sets can differ in length, and that the |
||||
# data in the datasets needn't be in order; they will be ordered |
||||
# by the plot along the X-axis. |
||||
projection = [ |
||||
6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13, |
||||
7, 9 |
||||
] |
||||
actual = [ |
||||
0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12, |
||||
15, 6, 4, 17, 2, 12 |
||||
] |
||||
|
||||
graph = SVG::Graph::Plot.new({ |
||||
:height => 500, |
||||
:width => 300, |
||||
:key => true, |
||||
:scale_x_integers => true, |
||||
:scale_y_integerrs => true, |
||||
}) |
||||
|
||||
graph.add_data({ |
||||
:data => projection |
||||
:title => 'Projected', |
||||
}) |
||||
|
||||
graph.add_data({ |
||||
:data => actual, |
||||
:title => 'Actual', |
||||
}) |
||||
|
||||
print graph.burn() |
||||
|
||||
= Description |
||||
|
||||
Produces a graph of scalar data. |
||||
|
||||
This object aims to allow you to easily create high quality |
||||
SVG[http://www.w3c.org/tr/svg] scalar plots. You can either use the |
||||
default style sheet or supply your own. Either way there are many options |
||||
which can be configured to give you control over how the graph is |
||||
generated - with or without a key, data elements at each point, title, |
||||
subtitle etc. |
||||
|
||||
= Examples |
||||
|
||||
http://www.germane-software/repositories/public/SVG/test/plot.rb |
||||
|
||||
= Notes |
||||
|
||||
The default stylesheet handles upto 10 data sets, if you |
||||
use more you must create your own stylesheet and add the |
||||
additional settings for the extra data sets. You will know |
||||
if you go over 10 data sets as they will have no style and |
||||
be in black. |
||||
|
||||
Unlike the other types of charts, data sets must contain x,y pairs: |
||||
|
||||
[1, 2] # A data set with 1 point: (1,2) |
||||
[1,2, 5,6] # A data set with 2 points: (1,2) and (5,6) |
||||
|
||||
= See also |
||||
|
||||
* SVG::Graph::Graph |
||||
* SVG::Graph::BarHorizontal |
||||
* SVG::Graph::Bar |
||||
* SVG::Graph::Line |
||||
* SVG::Graph::Pie |
||||
* SVG::Graph::TimeSeries |
||||
|
||||
== Author |
||||
|
||||
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> |
||||
|
||||
Copyright 2004 Sean E. Russell |
||||
This software is available under the Ruby license[LICENSE.txt]""" |
||||
|
||||
top_align = right_align = top_font = right_font = 1 |
||||
|
||||
"""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(): |
||||
doc = """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 |
||||
path = etree.SubElement(self.graph, 'path', { |
||||
'd': 'M%(x_start)f %(graph_height)f' |
||||
' %(lpath)s V%(graph_height)f Z' % vars(), |
||||
'class': 'fill%(line)d' % vars()}) |
||||
if self.draw_lines_between_points: |
||||
path = etree.SubElement(self.graph, 'path', { |
||||
'd': 'M%(x_start)f %(y_start)f %(lpath)s' % vars(), |
||||
'class': 'line%(line)d' % vars()}) |
||||
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 = etree.SubElement(self.graph, 'path', { |
||||
'd': 'M 0 %(start)s h%(stop)s' % vars(), |
||||
'class': 'constantLine'}) |
||||
if style: |
||||
path.set('style', style) |
||||
text = etree.SubElement(self.graph, 'text', { |
||||
'x': str(2), |
||||
'y': str(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'] |
||||
#locals().update(self.__transform_parameters) |
||||
#vars().update(self.__transform_parameters) |
||||
x = (x - x_min) * x_step |
||||
y = self.graph_height - (y - y_min) * y_step |
||||
return x, y |
||||
|
||||
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: |
||||
etree.SubElement(self.graph, 'circle', { |
||||
'cx': str(gx), |
||||
'cy': str(gy), |
||||
'r': '2.5', |
||||
'class': 'dataPoint%(line)s' % vars()}) |
||||
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,313 +1,313 @@
|
||||
#!python |
||||
import re |
||||
|
||||
from dateutil.parser import parse |
||||
from dateutil.relativedelta import relativedelta |
||||
from lxml import etree |
||||
|
||||
from pygal.graph import Graph |
||||
from util import grouper, date_range, divide_timedelta_float, TimeScale |
||||
|
||||
__all__ = ('Schedule') |
||||
|
||||
|
||||
class Schedule(Graph): |
||||
""" |
||||
# === For creating SVG plots of scalar temporal data |
||||
|
||||
= Synopsis |
||||
|
||||
require 'SVG/Graph/Schedule' |
||||
|
||||
# Data sets are label, start, end tripples. |
||||
data1 = [ |
||||
"Housesitting", "6/17/04", "6/19/04", |
||||
"Summer Session", "6/15/04", "8/15/04", |
||||
] |
||||
|
||||
graph = SVG::Graph::Schedule.new( { |
||||
:width => 640, |
||||
:height => 480, |
||||
:graph_title => title, |
||||
:show_graph_title => true, |
||||
:no_css => true, |
||||
:scale_x_integers => true, |
||||
:scale_y_integers => true, |
||||
:min_x_value => 0, |
||||
:min_y_value => 0, |
||||
:show_data_labels => true, |
||||
:show_x_guidelines => true, |
||||
:show_x_title => true, |
||||
:x_title => "Time", |
||||
:stagger_x_labels => true, |
||||
:stagger_y_labels => true, |
||||
:x_label_format => "%m/%d/%y", |
||||
}) |
||||
|
||||
graph.add_data({ |
||||
:data => data1, |
||||
:title => 'Data', |
||||
}) |
||||
|
||||
print graph.burn() |
||||
|
||||
= Description |
||||
|
||||
Produces a graph of temporal scalar data. |
||||
|
||||
= Examples |
||||
|
||||
http://www.germane-software/repositories/public/SVG/test/schedule.rb |
||||
|
||||
= Notes |
||||
|
||||
The default stylesheet handles upto 10 data sets, if you |
||||
use more you must create your own stylesheet and add the |
||||
additional settings for the extra data sets. You will know |
||||
if you go over 10 data sets as they will have no style and |
||||
be in black. |
||||
|
||||
Note that multiple data sets within the same chart can differ in |
||||
length, and that the data in the datasets needn't be in order; |
||||
they will be ordered by the plot along the X-axis. |
||||
|
||||
The dates must be parseable by ParseDate, but otherwise can be |
||||
any order of magnitude (seconds within the hour, or years) |
||||
|
||||
= See also |
||||
|
||||
* SVG::Graph::Graph |
||||
* SVG::Graph::BarHorizontal |
||||
* SVG::Graph::Bar |
||||
* SVG::Graph::Line |
||||
* SVG::Graph::Pie |
||||
* SVG::Graph::Plot |
||||
* SVG::Graph::TimeSeries |
||||
|
||||
== Author |
||||
|
||||
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> |
||||
|
||||
Copyright 2004 Sean E. Russell |
||||
This software is available under the Ruby license[LICENSE.txt] |
||||
|
||||
""" |
||||
|
||||
"The format string to be used to format the X axis labels" |
||||
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. |
||||
|
||||
# A data set with 1 point: Lunch from 12:30 to 14:00 |
||||
d1 = [ "Lunch", "12:30", "14:00" ] |
||||
# A data set with 2 points: "Cats" runs from 5/11/03 to 7/15/04, and |
||||
# "Henry V" runs from 6/12/03 to 8/20/03 |
||||
d2 = [ "Cats", "5/11/03", "7/15/04", |
||||
"Henry V", "6/12/03", "8/20/03" ] |
||||
|
||||
graph.add_data( |
||||
:data => d1, |
||||
:title => 'Meetings' |
||||
) |
||||
graph.add_data( |
||||
:data => d2, |
||||
:title => 'Plays' |
||||
) |
||||
|
||||
Note that the data must be in time,value pairs, |
||||
and that the date format |
||||
may be any date that is parseable by ParseDate. |
||||
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 |
||||
|
||||
y_mod = (subbar_height / 2) + (self.font_size / 2) |
||||
x_min, x_max, div = self._x_range() |
||||
x_range = x_max - x_min |
||||
width = (float(self.graph_width) - self.font_size * 2) |
||||
# 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) |
||||
|
||||
etree.SubElement(self.graph, 'rect', { |
||||
'x': str(bar_start), |
||||
'y': str(y), |
||||
'width': str(bar_width), |
||||
'height': str(subbar_height), |
||||
'class': 'fill%s' % (count + 1), |
||||
}) |
||||
|
||||
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] |
||||
#!python |
||||
import re |
||||
|
||||
from dateutil.parser import parse |
||||
from dateutil.relativedelta import relativedelta |
||||
from lxml import etree |
||||
|
||||
from pygal.graph import Graph |
||||
from util import grouper, date_range, divide_timedelta_float, TimeScale |
||||
|
||||
__all__ = ('Schedule') |
||||
|
||||
|
||||
class Schedule(Graph): |
||||
""" |
||||
# === For creating SVG plots of scalar temporal data |
||||
|
||||
= Synopsis |
||||
|
||||
require 'SVG/Graph/Schedule' |
||||
|
||||
# Data sets are label, start, end tripples. |
||||
data1 = [ |
||||
"Housesitting", "6/17/04", "6/19/04", |
||||
"Summer Session", "6/15/04", "8/15/04", |
||||
] |
||||
|
||||
graph = SVG::Graph::Schedule.new( { |
||||
:width => 640, |
||||
:height => 480, |
||||
:graph_title => title, |
||||
:show_graph_title => true, |
||||
:no_css => true, |
||||
:scale_x_integers => true, |
||||
:scale_y_integers => true, |
||||
:min_x_value => 0, |
||||
:min_y_value => 0, |
||||
:show_data_labels => true, |
||||
:show_x_guidelines => true, |
||||
:show_x_title => true, |
||||
:x_title => "Time", |
||||
:stagger_x_labels => true, |
||||
:stagger_y_labels => true, |
||||
:x_label_format => "%m/%d/%y", |
||||
}) |
||||
|
||||
graph.add_data({ |
||||
:data => data1, |
||||
:title => 'Data', |
||||
}) |
||||
|
||||
print graph.burn() |
||||
|
||||
= Description |
||||
|
||||
Produces a graph of temporal scalar data. |
||||
|
||||
= Examples |
||||
|
||||
http://www.germane-software/repositories/public/SVG/test/schedule.rb |
||||
|
||||
= Notes |
||||
|
||||
The default stylesheet handles upto 10 data sets, if you |
||||
use more you must create your own stylesheet and add the |
||||
additional settings for the extra data sets. You will know |
||||
if you go over 10 data sets as they will have no style and |
||||
be in black. |
||||
|
||||
Note that multiple data sets within the same chart can differ in |
||||
length, and that the data in the datasets needn't be in order; |
||||
they will be ordered by the plot along the X-axis. |
||||
|
||||
The dates must be parseable by ParseDate, but otherwise can be |
||||
any order of magnitude (seconds within the hour, or years) |
||||
|
||||
= See also |
||||
|
||||
* SVG::Graph::Graph |
||||
* SVG::Graph::BarHorizontal |
||||
* SVG::Graph::Bar |
||||
* SVG::Graph::Line |
||||
* SVG::Graph::Pie |
||||
* SVG::Graph::Plot |
||||
* SVG::Graph::TimeSeries |
||||
|
||||
== Author |
||||
|
||||
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> |
||||
|
||||
Copyright 2004 Sean E. Russell |
||||
This software is available under the Ruby license[LICENSE.txt] |
||||
|
||||
""" |
||||
|
||||
"The format string to be used to format the X axis labels" |
||||
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. |
||||
|
||||
# A data set with 1 point: Lunch from 12:30 to 14:00 |
||||
d1 = [ "Lunch", "12:30", "14:00" ] |
||||
# A data set with 2 points: "Cats" runs from 5/11/03 to 7/15/04, and |
||||
# "Henry V" runs from 6/12/03 to 8/20/03 |
||||
d2 = [ "Cats", "5/11/03", "7/15/04", |
||||
"Henry V", "6/12/03", "8/20/03" ] |
||||
|
||||
graph.add_data( |
||||
:data => d1, |
||||
:title => 'Meetings' |
||||
) |
||||
graph.add_data( |
||||
:data => d2, |
||||
:title => 'Plays' |
||||
) |
||||
|
||||
Note that the data must be in time,value pairs, |
||||
and that the date format |
||||
may be any date that is parseable by ParseDate. |
||||
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 |
||||
|
||||
y_mod = (subbar_height / 2) + (self.font_size / 2) |
||||
x_min, x_max, div = self._x_range() |
||||
x_range = x_max - x_min |
||||
width = (float(self.graph_width) - self.font_size * 2) |
||||
# 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) |
||||
|
||||
etree.SubElement(self.graph, 'rect', { |
||||
'x': str(bar_start), |
||||
'y': str(y), |
||||
'width': str(bar_width), |
||||
'height': str(subbar_height), |
||||
'class': 'fill%s' % (count + 1), |
||||
}) |
||||
|
||||
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 +1,199 @@
|
||||
#!/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()) |
||||
#!/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,178 +1,178 @@
|
||||
#!python |
||||
|
||||
import itertools |
||||
import datetime |
||||
# from itertools recipes (python documentation) |
||||
|
||||
|
||||
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)) |
||||
#!python |
||||
|
||||
import itertools |
||||
import datetime |
||||
# from itertools recipes (python documentation) |
||||
|
||||
|
||||
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,63 +1,63 @@
|
||||
#!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") |
||||
|
||||
_this_dir = os.path.dirname(__file__) |
||||
_readme = os.path.join(_this_dir, 'readme.txt') |
||||
_long_description = open(_readme).read().strip() |
||||
|
||||
# it seems that dateutil 2.0 only works under Python 3 |
||||
dateutil_req = ( |
||||
['python-dateutil>=1.4,<2.0dev'] if sys.version_info < (3, 0) |
||||
else ['python-dateutil>=2.0']) |
||||
|
||||
setup_params = dict( |
||||
name="pygal", |
||||
description="Python svg graph abstract layer", |
||||
long_description=_long_description, |
||||
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=[ |
||||
'cssutils>=0.9.8a3', |
||||
'lxml>=2.0', |
||||
] + dateutil_req, |
||||
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) |
||||
#!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") |
||||
|
||||
_this_dir = os.path.dirname(__file__) |
||||
_readme = os.path.join(_this_dir, 'readme.txt') |
||||
_long_description = open(_readme).read().strip() |
||||
|
||||
# it seems that dateutil 2.0 only works under Python 3 |
||||
dateutil_req = ( |
||||
['python-dateutil>=1.4,<2.0dev'] if sys.version_info < (3, 0) |
||||
else ['python-dateutil>=2.0']) |
||||
|
||||
setup_params = dict( |
||||
name="pygal", |
||||
description="Python svg graph abstract layer", |
||||
long_description=_long_description, |
||||
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=[ |
||||
'cssutils>=0.9.8a3', |
||||
'lxml>=2.0', |
||||
] + dateutil_req, |
||||
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) |
||||
|
Loading…
Reference in new issue