mirror of https://github.com/Kozea/pygal.git
Florian Mounier
14 years ago
17 changed files with 2750 additions and 2751 deletions
@ -1,8 +1,8 @@ |
|||||||
#!python |
#!python |
||||||
# -*- coding: utf-8 -*- |
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
""" |
""" |
||||||
pygal package. |
pygal package. |
||||||
""" |
""" |
||||||
|
|
||||||
__all__ = ('graph', 'plot', 'time_series', 'bar', 'pie', 'schedule', 'util') |
__all__ = ('graph', 'plot', 'time_series', 'bar', 'pie', 'schedule', 'util') |
||||||
|
@ -1,68 +1,68 @@ |
|||||||
.bar .dataPointLabel { |
.bar .dataPointLabel { |
||||||
fill-opacity: 0; |
fill-opacity: 0; |
||||||
-webkit-transition: 250ms; |
-webkit-transition: 250ms; |
||||||
} |
} |
||||||
|
|
||||||
.bar:hover .dataPointLabel { |
.bar:hover .dataPointLabel { |
||||||
fill-opacity: 0.9; |
fill-opacity: 0.9; |
||||||
fill: #000000; |
fill: #000000; |
||||||
} |
} |
||||||
|
|
||||||
.key, .fill { |
.key, .fill { |
||||||
fill-opacity: 0.5; |
fill-opacity: 0.5; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
-webkit-transition: 250ms; |
-webkit-transition: 250ms; |
||||||
} |
} |
||||||
|
|
||||||
.fill:hover { |
.fill:hover { |
||||||
fill-opacity: 0.25; |
fill-opacity: 0.25; |
||||||
} |
} |
||||||
|
|
||||||
.key1, .fill1 { |
.key1, .fill1 { |
||||||
fill: #ff0000; |
fill: #ff0000; |
||||||
} |
} |
||||||
|
|
||||||
.key2, .fill2 { |
.key2, .fill2 { |
||||||
fill: #0000ff; |
fill: #0000ff; |
||||||
} |
} |
||||||
|
|
||||||
.key3, .fill3 { |
.key3, .fill3 { |
||||||
fill: #00ff00; |
fill: #00ff00; |
||||||
} |
} |
||||||
|
|
||||||
.key4, .fill4 { |
.key4, .fill4 { |
||||||
fill: #ffcc00; |
fill: #ffcc00; |
||||||
} |
} |
||||||
|
|
||||||
.key5, .fill5 { |
.key5, .fill5 { |
||||||
fill: #00ccff; |
fill: #00ccff; |
||||||
} |
} |
||||||
|
|
||||||
.key6, .fill6 { |
.key6, .fill6 { |
||||||
fill: #ff00ff; |
fill: #ff00ff; |
||||||
} |
} |
||||||
|
|
||||||
.key7, .fill7 { |
.key7, .fill7 { |
||||||
fill: #00ffff; |
fill: #00ffff; |
||||||
} |
} |
||||||
|
|
||||||
.key8, .fill8 { |
.key8, .fill8 { |
||||||
fill: #ffff00; |
fill: #ffff00; |
||||||
} |
} |
||||||
|
|
||||||
.key9, .fill9 { |
.key9, .fill9 { |
||||||
fill: #cc6666; |
fill: #cc6666; |
||||||
} |
} |
||||||
|
|
||||||
.key10, .fill10 { |
.key10, .fill10 { |
||||||
fill: #663399; |
fill: #663399; |
||||||
} |
} |
||||||
|
|
||||||
.key11, .fill11 { |
.key11, .fill11 { |
||||||
fill: #339900; |
fill: #339900; |
||||||
} |
} |
||||||
|
|
||||||
.key12, .fill12 { |
.key12, .fill12 { |
||||||
fill: #9966FF; |
fill: #9966FF; |
||||||
} |
} |
||||||
|
@ -1,82 +1,82 @@ |
|||||||
import cssutils |
import cssutils |
||||||
|
|
||||||
SVG = 'SVG 1.1' # http://www.w3.org/TR/SVG11/styling.html |
SVG = 'SVG 1.1' # http://www.w3.org/TR/SVG11/styling.html |
||||||
|
|
||||||
macros = { |
macros = { |
||||||
'paint': 'none|currentColor|{color}', |
'paint': 'none|currentColor|{color}', |
||||||
'unitidentifier': 'em|ex|px|pt|pc|cm|mm|in|%', |
'unitidentifier': 'em|ex|px|pt|pc|cm|mm|in|%', |
||||||
'length': '{positivenum}({unitidentifier})?', |
'length': '{positivenum}({unitidentifier})?', |
||||||
'dasharray': '{positivenum}(\s*,\s*{positivenum})*', |
'dasharray': '{positivenum}(\s*,\s*{positivenum})*', |
||||||
# a number greater-than or equal to one |
# a number greater-than or equal to one |
||||||
'number-ge-one': '{[1-9][0-9]*(\.[0-9]+)?', |
'number-ge-one': '{[1-9][0-9]*(\.[0-9]+)?', |
||||||
} |
} |
||||||
properties = { |
properties = { |
||||||
# Clipping, Masking, and Compositing |
# Clipping, Masking, and Compositing |
||||||
'clip-path': '{uri}|none|inherit', |
'clip-path': '{uri}|none|inherit', |
||||||
'clip-rule': 'nonzero|evenodd|inherit', |
'clip-rule': 'nonzero|evenodd|inherit', |
||||||
'mask': '{uri}|none|inherit', |
'mask': '{uri}|none|inherit', |
||||||
'opacity': '{num}|inherit', |
'opacity': '{num}|inherit', |
||||||
|
|
||||||
# Filter Effects |
# Filter Effects |
||||||
'enable-background': 'accumulate|new(\s+{num}){0,4}|inherit', |
'enable-background': 'accumulate|new(\s+{num}){0,4}|inherit', |
||||||
'filter': '{uri}|none|inherit', |
'filter': '{uri}|none|inherit', |
||||||
'flood-color': 'currentColor|{color}|inherit', |
'flood-color': 'currentColor|{color}|inherit', |
||||||
'flood-opacity': '{num}|inherit', |
'flood-opacity': '{num}|inherit', |
||||||
'lighting-color': 'currentColor|{color}|inherit', |
'lighting-color': 'currentColor|{color}|inherit', |
||||||
|
|
||||||
# Gradient Properties |
# Gradient Properties |
||||||
'stop-color': 'currentColor|{color}|inherit', |
'stop-color': 'currentColor|{color}|inherit', |
||||||
'stop-opacity': '{num}|inherit', |
'stop-opacity': '{num}|inherit', |
||||||
|
|
||||||
# Interactivity Properties |
# Interactivity Properties |
||||||
'pointer-events': ('visiblePainted|visibleFill|visibleStroke' |
'pointer-events': ('visiblePainted|visibleFill|visibleStroke' |
||||||
'|visible|painted|fill|stroke|all|none|inherit'), |
'|visible|painted|fill|stroke|all|none|inherit'), |
||||||
|
|
||||||
# Color and Pointing Properties |
# Color and Pointing Properties |
||||||
'color-interpolation': 'auto|sRGB|linearRGB|inherit', |
'color-interpolation': 'auto|sRGB|linearRGB|inherit', |
||||||
'color-interpolation-filters': 'auto|sRGB|linearRGB|inherit', |
'color-interpolation-filters': 'auto|sRGB|linearRGB|inherit', |
||||||
'color-rendering': 'auto|optimizeSpeed|optimizeQuality|inherit', |
'color-rendering': 'auto|optimizeSpeed|optimizeQuality|inherit', |
||||||
'shape-rendering': ('auto|optimizeSpeed|crispEdges|' |
'shape-rendering': ('auto|optimizeSpeed|crispEdges|' |
||||||
'geometricPrecision|inherit'), |
'geometricPrecision|inherit'), |
||||||
'text-rendering': ('auto|optimizeSpeed|optimizeLegibility' |
'text-rendering': ('auto|optimizeSpeed|optimizeLegibility' |
||||||
'|geometricPrecision|inherit'), |
'|geometricPrecision|inherit'), |
||||||
'fill': '{paint}', |
'fill': '{paint}', |
||||||
'fill-opacity': '{num}|inherit', |
'fill-opacity': '{num}|inherit', |
||||||
'fill-rule': 'nonzero|evenodd|inherit', |
'fill-rule': 'nonzero|evenodd|inherit', |
||||||
'image-rendering': 'auto|optimizeSpeed|optimizeQuality|inherit', |
'image-rendering': 'auto|optimizeSpeed|optimizeQuality|inherit', |
||||||
'marker': 'none|inherit|{uri}', |
'marker': 'none|inherit|{uri}', |
||||||
'marker-end': 'none|inherit|{uri}', |
'marker-end': 'none|inherit|{uri}', |
||||||
'marker-mid': 'none|inherit|{uri}', |
'marker-mid': 'none|inherit|{uri}', |
||||||
'marker-start': 'none|inherit|{uri}', |
'marker-start': 'none|inherit|{uri}', |
||||||
'shape-rendering': ('auto|optimizeSpeed|crispEdges|' |
'shape-rendering': ('auto|optimizeSpeed|crispEdges|' |
||||||
'geometricPrecision|inherit'), |
'geometricPrecision|inherit'), |
||||||
'stroke': '{paint}', |
'stroke': '{paint}', |
||||||
'stroke-dasharray': 'none|{dasharray}|inherit', |
'stroke-dasharray': 'none|{dasharray}|inherit', |
||||||
'stroke-dashoffset': '{length}|inherit', |
'stroke-dashoffset': '{length}|inherit', |
||||||
'stroke-linecap': 'butt|round|square|inherit', |
'stroke-linecap': 'butt|round|square|inherit', |
||||||
'stroke-linejoin': 'miter|round|bevel|inherit', |
'stroke-linejoin': 'miter|round|bevel|inherit', |
||||||
'stroke-miterlimit': '{number-ge-one}|inherit', |
'stroke-miterlimit': '{number-ge-one}|inherit', |
||||||
'stroke-opacity': '{num}|inherit', |
'stroke-opacity': '{num}|inherit', |
||||||
'stroke-width': '{length}|inherit', |
'stroke-width': '{length}|inherit', |
||||||
'text-rendering': ('auto|optimizeSpeed|optimizeLegibility' |
'text-rendering': ('auto|optimizeSpeed|optimizeLegibility' |
||||||
'|geometricPrecision|inherit'), |
'|geometricPrecision|inherit'), |
||||||
|
|
||||||
# Text Properties |
# Text Properties |
||||||
'alignment-baseline': ('auto|baseline|before-edge|text-before-edge|' |
'alignment-baseline': ('auto|baseline|before-edge|text-before-edge|' |
||||||
'middle|central|after-edge|text-after-edge|' |
'middle|central|after-edge|text-after-edge|' |
||||||
'ideographic|alphabetic|hanging|mathematical' |
'ideographic|alphabetic|hanging|mathematical' |
||||||
'|inherit'), |
'|inherit'), |
||||||
'baseline-shift': 'baseline|sub|super|{percentage}|{length}|inherit', |
'baseline-shift': 'baseline|sub|super|{percentage}|{length}|inherit', |
||||||
'dominant-baseline': ('auto|use-script|no-change|reset-size|ideographic|' |
'dominant-baseline': ('auto|use-script|no-change|reset-size|ideographic|' |
||||||
'alphabetic|hanging||mathematical|central|middle|' |
'alphabetic|hanging||mathematical|central|middle|' |
||||||
'text-after-edge|text-before-edge|inherit'), |
'text-after-edge|text-before-edge|inherit'), |
||||||
'glyph-orientation-horizontal': '{angle}|inherit', |
'glyph-orientation-horizontal': '{angle}|inherit', |
||||||
'glyph-orientation-vertical': 'auto|{angle}|inherit', |
'glyph-orientation-vertical': 'auto|{angle}|inherit', |
||||||
'kerning': 'auto|{length}|inherit', |
'kerning': 'auto|{length}|inherit', |
||||||
'text-anchor': 'start|middle|end|inherit', |
'text-anchor': 'start|middle|end|inherit', |
||||||
'writing-mode': 'lr-tb|rl-tb|tb-rl|lr|rl|tb|inherit', |
'writing-mode': 'lr-tb|rl-tb|tb-rl|lr|rl|tb|inherit', |
||||||
} |
} |
||||||
|
|
||||||
cssutils.profile.addProfile(SVG, properties, macros) |
cssutils.profile.addProfile(SVG, properties, macros) |
||||||
|
|
||||||
cssutils.profile.defaultProfiles = [SVG, cssutils.profile.CSS_LEVEL_2] |
cssutils.profile.defaultProfiles = [SVG, cssutils.profile.CSS_LEVEL_2] |
||||||
|
@ -1,87 +1,87 @@ |
|||||||
/* |
/* |
||||||
$Id$ |
$Id$ |
||||||
|
|
||||||
Base styles for pygal.Graph |
Base styles for pygal.Graph |
||||||
*/ |
*/ |
||||||
|
|
||||||
* { |
* { |
||||||
font-family: monospace; |
font-family: monospace; |
||||||
} |
} |
||||||
.svgBackground{ |
.svgBackground{ |
||||||
fill:#ffffff; |
fill:#ffffff; |
||||||
} |
} |
||||||
.graphBackground{ |
.graphBackground{ |
||||||
fill:#ffffff; |
fill:#ffffff; |
||||||
} |
} |
||||||
|
|
||||||
/* graphs titles */ |
/* graphs titles */ |
||||||
.mainTitle{ |
.mainTitle{ |
||||||
text-anchor: middle; |
text-anchor: middle; |
||||||
fill: #000000; |
fill: #000000; |
||||||
font-size: %(title_font_size)dpx; |
font-size: %(title_font_size)dpx; |
||||||
font-weight: normal; |
font-weight: normal; |
||||||
} |
} |
||||||
.subTitle{ |
.subTitle{ |
||||||
text-anchor: middle; |
text-anchor: middle; |
||||||
fill: #999999; |
fill: #999999; |
||||||
font-size: %(subtitle_font_size)dpx; |
font-size: %(subtitle_font_size)dpx; |
||||||
font-weight: normal; |
font-weight: normal; |
||||||
} |
} |
||||||
|
|
||||||
.axis{ |
.axis{ |
||||||
stroke: #000000; |
stroke: #000000; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
|
|
||||||
.guideLines{ |
.guideLines{ |
||||||
stroke: #eee; |
stroke: #eee; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
stroke-dasharray: 5,5; |
stroke-dasharray: 5,5; |
||||||
} |
} |
||||||
|
|
||||||
.xAxisLabels{ |
.xAxisLabels{ |
||||||
text-anchor: middle; |
text-anchor: middle; |
||||||
fill: #000000; |
fill: #000000; |
||||||
font-size: %(x_label_font_size)dpx; |
font-size: %(x_label_font_size)dpx; |
||||||
font-weight: normal; |
font-weight: normal; |
||||||
} |
} |
||||||
|
|
||||||
.yAxisLabels{ |
.yAxisLabels{ |
||||||
text-anchor: end; |
text-anchor: end; |
||||||
fill: #000000; |
fill: #000000; |
||||||
font-size: %(y_label_font_size)dpx; |
font-size: %(y_label_font_size)dpx; |
||||||
font-weight: normal; |
font-weight: normal; |
||||||
} |
} |
||||||
|
|
||||||
.xAxisTitle{ |
.xAxisTitle{ |
||||||
text-anchor: middle; |
text-anchor: middle; |
||||||
fill: #ff0000; |
fill: #ff0000; |
||||||
font-size: %(x_title_font_size)dpx; |
font-size: %(x_title_font_size)dpx; |
||||||
font-weight: normal; |
font-weight: normal; |
||||||
} |
} |
||||||
|
|
||||||
.yAxisTitle{ |
.yAxisTitle{ |
||||||
fill: #ff0000; |
fill: #ff0000; |
||||||
text-anchor: middle; |
text-anchor: middle; |
||||||
font-size: %(y_title_font_size)dpx; |
font-size: %(y_title_font_size)dpx; |
||||||
font-weight: normal; |
font-weight: normal; |
||||||
} |
} |
||||||
|
|
||||||
.dataPointLabel{ |
.dataPointLabel{ |
||||||
text-anchor:middle; |
text-anchor:middle; |
||||||
font-size: 10px; |
font-size: 10px; |
||||||
font-weight: normal; |
font-weight: normal; |
||||||
} |
} |
||||||
|
|
||||||
.staggerGuideLine{ |
.staggerGuideLine{ |
||||||
fill: none; |
fill: none; |
||||||
stroke: #000000; |
stroke: #000000; |
||||||
stroke-width: 0.5px; |
stroke-width: 0.5px; |
||||||
} |
} |
||||||
|
|
||||||
.keyText{ |
.keyText{ |
||||||
fill: #000000; |
fill: #000000; |
||||||
text-anchor:start; |
text-anchor:start; |
||||||
font-size: %(key_font_size)dpx; |
font-size: %(key_font_size)dpx; |
||||||
font-weight: normal; |
font-weight: normal; |
||||||
} |
} |
||||||
|
@ -1,178 +1,178 @@ |
|||||||
#!python |
#!python |
||||||
|
|
||||||
# $Id$ |
# $Id$ |
||||||
|
|
||||||
from operator import itemgetter, add |
from operator import itemgetter, add |
||||||
from lxml import etree |
from lxml import etree |
||||||
|
|
||||||
from util import flatten, float_range |
from util import flatten, float_range |
||||||
from pygal.graph import Graph |
from pygal.graph import Graph |
||||||
|
|
||||||
|
|
||||||
class Line(Graph): |
class Line(Graph): |
||||||
"""Line Graph""" |
"""Line Graph""" |
||||||
|
|
||||||
"""Show a small circle on the graph where the line goes from one point to |
"""Show a small circle on the graph where the line goes from one point to |
||||||
the next""" |
the next""" |
||||||
show_data_points = True |
show_data_points = True |
||||||
show_data_values = True |
show_data_values = True |
||||||
"""Accumulates each data set. (i.e. Each point increased by sum of all |
"""Accumulates each data set. (i.e. Each point increased by sum of all |
||||||
previous series at same point).""" |
previous series at same point).""" |
||||||
stacked = False |
stacked = False |
||||||
"Fill in the area under the plot" |
"Fill in the area under the plot" |
||||||
area_fill = False |
area_fill = False |
||||||
|
|
||||||
scale_divisions = None |
scale_divisions = None |
||||||
|
|
||||||
#override some defaults |
#override some defaults |
||||||
top_align = top_font = right_align = right_font = True |
top_align = top_font = right_align = right_font = True |
||||||
|
|
||||||
stylesheet_names = Graph.stylesheet_names + ['plot.css'] |
stylesheet_names = Graph.stylesheet_names + ['plot.css'] |
||||||
|
|
||||||
def max_value(self): |
def max_value(self): |
||||||
data = map(itemgetter('data'), self.data) |
data = map(itemgetter('data'), self.data) |
||||||
if self.stacked: |
if self.stacked: |
||||||
data = self.get_cumulative_data() |
data = self.get_cumulative_data() |
||||||
return max(flatten(data)) |
return max(flatten(data)) |
||||||
|
|
||||||
def min_value(self): |
def min_value(self): |
||||||
if self.min_scale_value: |
if self.min_scale_value: |
||||||
return self.min_scale_value |
return self.min_scale_value |
||||||
data = map(itemgetter('data'), self.data) |
data = map(itemgetter('data'), self.data) |
||||||
if self.stacked: |
if self.stacked: |
||||||
data = self.get_cumulative_data() |
data = self.get_cumulative_data() |
||||||
return min(flatten(data)) |
return min(flatten(data)) |
||||||
|
|
||||||
def get_cumulative_data(self): |
def get_cumulative_data(self): |
||||||
"""Get the data as it will be charted. The first set will be |
"""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 |
the actual first data set. The second will be the sum of the |
||||||
first and the second, etc.""" |
first and the second, etc.""" |
||||||
sets = map(itemgetter('data'), self.data) |
sets = map(itemgetter('data'), self.data) |
||||||
if not sets: |
if not sets: |
||||||
return |
return |
||||||
sum = sets.pop(0) |
sum = sets.pop(0) |
||||||
yield sum |
yield sum |
||||||
while sets: |
while sets: |
||||||
sum = map(add, sets.pop(0)) |
sum = map(add, sets.pop(0)) |
||||||
yield sum |
yield sum |
||||||
|
|
||||||
def get_x_labels(self): |
def get_x_labels(self): |
||||||
return self.fields |
return self.fields |
||||||
|
|
||||||
def calculate_left_margin(self): |
def calculate_left_margin(self): |
||||||
super(self.__class__, self).calculate_left_margin() |
super(self.__class__, self).calculate_left_margin() |
||||||
label_left = len(self.fields[0]) / 2 * self.font_size * 0.6 |
label_left = len(self.fields[0]) / 2 * self.font_size * 0.6 |
||||||
self.border_left = max(label_left, self.border_left) |
self.border_left = max(label_left, self.border_left) |
||||||
|
|
||||||
def get_y_label_values(self): |
def get_y_label_values(self): |
||||||
max_value = self.max_value() |
max_value = self.max_value() |
||||||
min_value = self.min_value() |
min_value = self.min_value() |
||||||
range = max_value - min_value |
range = max_value - min_value |
||||||
top_pad = (range / 20.0) or 10 |
top_pad = (range / 20.0) or 10 |
||||||
scale_range = (max_value + top_pad) - min_value |
scale_range = (max_value + top_pad) - min_value |
||||||
|
|
||||||
scale_division = self.scale_divisions or (scale_range / 10.0) |
scale_division = self.scale_divisions or (scale_range / 10.0) |
||||||
|
|
||||||
if self.scale_integers: |
if self.scale_integers: |
||||||
scale_division = min(1, round(scale_division)) |
scale_division = min(1, round(scale_division)) |
||||||
|
|
||||||
if max_value % scale_division == 0: |
if max_value % scale_division == 0: |
||||||
max_value += scale_division |
max_value += scale_division |
||||||
labels = tuple(float_range(min_value, max_value, scale_division)) |
labels = tuple(float_range(min_value, max_value, scale_division)) |
||||||
return labels |
return labels |
||||||
|
|
||||||
def get_y_labels(self): |
def get_y_labels(self): |
||||||
return map(str, self.get_y_label_values()) |
return map(str, self.get_y_label_values()) |
||||||
|
|
||||||
def calc_coords(self, field, value, width=None, height=None): |
def calc_coords(self, field, value, width=None, height=None): |
||||||
if width is None: |
if width is None: |
||||||
width = self.field_width |
width = self.field_width |
||||||
if height is None: |
if height is None: |
||||||
height = self.field_height |
height = self.field_height |
||||||
coords = dict( |
coords = dict( |
||||||
x=width * field, |
x=width * field, |
||||||
y=self.graph_height - value * height, |
y=self.graph_height - value * height, |
||||||
) |
) |
||||||
return coords |
return coords |
||||||
|
|
||||||
def draw_data(self): |
def draw_data(self): |
||||||
min_value = self.min_value() |
min_value = self.min_value() |
||||||
field_height = self.graph_height - self.font_size * 2 * self.top_font |
field_height = self.graph_height - self.font_size * 2 * self.top_font |
||||||
|
|
||||||
y_label_values = self.get_y_label_values() |
y_label_values = self.get_y_label_values() |
||||||
y_label_span = max(y_label_values) - min(y_label_values) |
y_label_span = max(y_label_values) - min(y_label_values) |
||||||
field_height /= float(y_label_span) |
field_height /= float(y_label_span) |
||||||
|
|
||||||
field_width = self.field_width() |
field_width = self.field_width() |
||||||
#line = len(self.data) |
#line = len(self.data) |
||||||
|
|
||||||
prev_sum = [0] * len(self.fields) |
prev_sum = [0] * len(self.fields) |
||||||
cum_sum = [-min_value] * len(self.fields) |
cum_sum = [-min_value] * len(self.fields) |
||||||
|
|
||||||
coord_format = lambda c: '%(x)s %(y)s' % c |
coord_format = lambda c: '%(x)s %(y)s' % c |
||||||
|
|
||||||
for line_n, data in reversed(list(enumerate(self.data, 1))): |
for line_n, data in reversed(list(enumerate(self.data, 1))): |
||||||
apath = '' |
apath = '' |
||||||
|
|
||||||
if not self.stacked: |
if not self.stacked: |
||||||
cum_sum = [-min_value] * len(self.fields) |
cum_sum = [-min_value] * len(self.fields) |
||||||
|
|
||||||
cum_sum = map(add, cum_sum, data['data']) |
cum_sum = map(add, cum_sum, data['data']) |
||||||
get_coords = lambda (i, val): self.calc_coords(i, |
get_coords = lambda (i, val): self.calc_coords(i, |
||||||
val, |
val, |
||||||
field_width, |
field_width, |
||||||
field_height) |
field_height) |
||||||
coords = map(get_coords, enumerate(cum_sum)) |
coords = map(get_coords, enumerate(cum_sum)) |
||||||
paths = map(coord_format, coords) |
paths = map(coord_format, coords) |
||||||
line_path = ' '.join(paths) |
line_path = ' '.join(paths) |
||||||
|
|
||||||
if self.area_fill: |
if self.area_fill: |
||||||
# to draw the area, we'll use the line above, followed by |
# to draw the area, we'll use the line above, followed by |
||||||
# tracing the bottom from right to left |
# tracing the bottom from right to left |
||||||
if self.stacked: |
if self.stacked: |
||||||
prev_sum_rev = list(enumerate(prev_sum)).reversed() |
prev_sum_rev = list(enumerate(prev_sum)).reversed() |
||||||
coords = map(get_coords, prev_sum_rev) |
coords = map(get_coords, prev_sum_rev) |
||||||
paths = map(coord_format, coords) |
paths = map(coord_format, coords) |
||||||
area_path = ' '.join(paths) |
area_path = ' '.join(paths) |
||||||
origin = paths[-1] |
origin = paths[-1] |
||||||
else: |
else: |
||||||
area_path = "V%(graph_height)s" % vars(self) |
area_path = "V%(graph_height)s" % vars(self) |
||||||
origin = coord_format(get_coords((0, 0))) |
origin = coord_format(get_coords((0, 0))) |
||||||
|
|
||||||
d = ' '.join(( |
d = ' '.join(( |
||||||
'M', |
'M', |
||||||
origin, |
origin, |
||||||
'L', |
'L', |
||||||
line_path, |
line_path, |
||||||
area_path, |
area_path, |
||||||
'Z' |
'Z' |
||||||
)) |
)) |
||||||
etree.SubElement(self.graph, 'path', { |
etree.SubElement(self.graph, 'path', { |
||||||
'class': 'fill%(line_n)s' % vars(), |
'class': 'fill%(line_n)s' % vars(), |
||||||
'd': d, |
'd': d, |
||||||
}) |
}) |
||||||
|
|
||||||
# now draw the line itself |
# now draw the line itself |
||||||
etree.SubElement(self.graph, 'path', { |
etree.SubElement(self.graph, 'path', { |
||||||
'd': 'M0 %s L%s' % (self.graph_height, line_path), |
'd': 'M0 %s L%s' % (self.graph_height, line_path), |
||||||
'class': 'line%(line_n)s' % vars(), |
'class': 'line%(line_n)s' % vars(), |
||||||
}) |
}) |
||||||
|
|
||||||
if self.show_data_points or self.show_data_values: |
if self.show_data_points or self.show_data_values: |
||||||
for i, value in enumerate(cum_sum): |
for i, value in enumerate(cum_sum): |
||||||
if self.show_data_points: |
if self.show_data_points: |
||||||
circle = etree.SubElement( |
circle = etree.SubElement( |
||||||
self.graph, |
self.graph, |
||||||
'circle', |
'circle', |
||||||
{'class': 'dataPoint%(line_n)s' % vars()}, |
{'class': 'dataPoint%(line_n)s' % vars()}, |
||||||
cx=str(field_width * i), |
cx=str(field_width * i), |
||||||
cy=str(self.graph_height - value * field_height), |
cy=str(self.graph_height - value * field_height), |
||||||
r='2.5', |
r='2.5', |
||||||
) |
) |
||||||
self.make_datapoint_text( |
self.make_datapoint_text( |
||||||
field_width * i, |
field_width * i, |
||||||
self.graph_height - value * field_height - 6, |
self.graph_height - value * field_height - 6, |
||||||
value + min_value |
value + min_value |
||||||
) |
) |
||||||
|
|
||||||
prev_sum = list(cum_sum) |
prev_sum = list(cum_sum) |
||||||
|
@ -1,81 +1,81 @@ |
|||||||
.dataPointLabel{ |
.dataPointLabel{ |
||||||
fill: #000000; |
fill: #000000; |
||||||
text-anchor:middle; |
text-anchor:middle; |
||||||
font-size: %(datapoint_font_size)spx; |
font-size: %(datapoint_font_size)spx; |
||||||
font-family: "Arial", sans-serif; |
font-family: "Arial", sans-serif; |
||||||
font-weight: normal; |
font-weight: normal; |
||||||
} |
} |
||||||
|
|
||||||
/* key - MUST match fill styles */ |
/* key - MUST match fill styles */ |
||||||
.key1,.fill1{ |
.key1,.fill1{ |
||||||
fill: #ff0000; |
fill: #ff0000; |
||||||
fill-opacity: 0.7; |
fill-opacity: 0.7; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key2,.fill2{ |
.key2,.fill2{ |
||||||
fill: #0000ff; |
fill: #0000ff; |
||||||
fill-opacity: 0.7; |
fill-opacity: 0.7; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key3,.fill3{ |
.key3,.fill3{ |
||||||
fill-opacity: 0.7; |
fill-opacity: 0.7; |
||||||
fill: #00ff00; |
fill: #00ff00; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key4,.fill4{ |
.key4,.fill4{ |
||||||
fill-opacity: 0.7; |
fill-opacity: 0.7; |
||||||
fill: #ffcc00; |
fill: #ffcc00; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key5,.fill5{ |
.key5,.fill5{ |
||||||
fill-opacity: 0.7; |
fill-opacity: 0.7; |
||||||
fill: #00ccff; |
fill: #00ccff; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key6,.fill6{ |
.key6,.fill6{ |
||||||
fill-opacity: 0.7; |
fill-opacity: 0.7; |
||||||
fill: #ff00ff; |
fill: #ff00ff; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key7,.fill7{ |
.key7,.fill7{ |
||||||
fill-opacity: 0.7; |
fill-opacity: 0.7; |
||||||
fill: #00ff99; |
fill: #00ff99; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key8,.fill8{ |
.key8,.fill8{ |
||||||
fill-opacity: 0.7; |
fill-opacity: 0.7; |
||||||
fill: #ffff00; |
fill: #ffff00; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key9,.fill9{ |
.key9,.fill9{ |
||||||
fill-opacity: 0.7; |
fill-opacity: 0.7; |
||||||
fill: #cc6666; |
fill: #cc6666; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key10,.fill10{ |
.key10,.fill10{ |
||||||
fill-opacity: 0.7; |
fill-opacity: 0.7; |
||||||
fill: #663399; |
fill: #663399; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key11,.fill11{ |
.key11,.fill11{ |
||||||
fill-opacity: 0.7; |
fill-opacity: 0.7; |
||||||
fill: #339900; |
fill: #339900; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key12,.fill12{ |
.key12,.fill12{ |
||||||
fill-opacity: 0.7; |
fill-opacity: 0.7; |
||||||
fill: #9966FF; |
fill: #9966FF; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
|
@ -1,302 +1,302 @@ |
|||||||
import math |
import math |
||||||
import itertools |
import itertools |
||||||
from lxml import etree |
from lxml import etree |
||||||
from pygal.graph import Graph |
from pygal.graph import Graph |
||||||
|
|
||||||
|
|
||||||
def robust_add(a, b): |
def robust_add(a, b): |
||||||
"Add numbers a and b, treating None as 0" |
"Add numbers a and b, treating None as 0" |
||||||
if a is None: |
if a is None: |
||||||
a = 0 |
a = 0 |
||||||
if b is None: |
if b is None: |
||||||
b = 0 |
b = 0 |
||||||
return a + b |
return a + b |
||||||
|
|
||||||
RADIANS = math.pi / 180 |
RADIANS = math.pi / 180 |
||||||
|
|
||||||
|
|
||||||
class Pie(Graph): |
class Pie(Graph): |
||||||
""" |
""" |
||||||
A presentation-quality SVG pie graph |
A presentation-quality SVG pie graph |
||||||
|
|
||||||
Synopsis |
Synopsis |
||||||
======== |
======== |
||||||
|
|
||||||
from pygal.pie import Pie |
from pygal.pie import Pie |
||||||
fields = ['Jan', 'Feb', 'Mar'] |
fields = ['Jan', 'Feb', 'Mar'] |
||||||
|
|
||||||
data_sales_02 = [12, 45, 21] |
data_sales_02 = [12, 45, 21] |
||||||
|
|
||||||
graph = Pie(dict( |
graph = Pie(dict( |
||||||
height = 500, |
height = 500, |
||||||
width = 300, |
width = 300, |
||||||
fields = fields)) |
fields = fields)) |
||||||
graph.add_data({'data': data_sales_02, 'title': 'Sales 2002'}) |
graph.add_data({'data': data_sales_02, 'title': 'Sales 2002'}) |
||||||
print "Content-type" image/svg+xml\r\n\r\n' |
print "Content-type" image/svg+xml\r\n\r\n' |
||||||
print graph.burn() |
print graph.burn() |
||||||
|
|
||||||
Description |
Description |
||||||
=========== |
=========== |
||||||
This object aims to allow you to easily create high quality |
This object aims to allow you to easily create high quality |
||||||
SVG pie graphs. You can either use the default style sheet |
SVG pie graphs. You can either use the default style sheet |
||||||
or supply your own. Either way there are many options which can |
or supply your own. Either way there are many options which can |
||||||
be configured to give you control over how the graph is |
be configured to give you control over how the graph is |
||||||
generated - with or without a key, display percent on pie chart, |
generated - with or without a key, display percent on pie chart, |
||||||
title, subtitle etc. |
title, subtitle etc. |
||||||
""" |
""" |
||||||
|
|
||||||
"if true, displays a drop shadow for the chart" |
"if true, displays a drop shadow for the chart" |
||||||
show_shadow = True |
show_shadow = True |
||||||
"Sets the offset of the shadow from the pie chart" |
"Sets the offset of the shadow from the pie chart" |
||||||
shadow_offset = 10 |
shadow_offset = 10 |
||||||
|
|
||||||
show_data_labels = False |
show_data_labels = False |
||||||
"If true, display the actual field values in the data labels" |
"If true, display the actual field values in the data labels" |
||||||
show_actual_values = False |
show_actual_values = False |
||||||
|
|
||||||
("If true, display the percentage value of" |
("If true, display the percentage value of" |
||||||
"each pie wedge in the data labels") |
"each pie wedge in the data labels") |
||||||
show_percent = True |
show_percent = True |
||||||
|
|
||||||
"If true, display the labels in the key" |
"If true, display the labels in the key" |
||||||
show_key_data_labels = True |
show_key_data_labels = True |
||||||
"If true, display the actual value of the field in the key" |
"If true, display the actual value of the field in the key" |
||||||
show_key_actual_values = True |
show_key_actual_values = True |
||||||
"If true, display the percentage value of the wedges in the key" |
"If true, display the percentage value of the wedges in the key" |
||||||
show_key_percent = False |
show_key_percent = False |
||||||
|
|
||||||
"If true, explode the pie (put space between the wedges)" |
"If true, explode the pie (put space between the wedges)" |
||||||
expanded = False |
expanded = False |
||||||
"If true, expand the largest pie wedge" |
"If true, expand the largest pie wedge" |
||||||
expand_greatest = False |
expand_greatest = False |
||||||
"The amount of space between expanded wedges" |
"The amount of space between expanded wedges" |
||||||
expand_gap = 10 |
expand_gap = 10 |
||||||
|
|
||||||
show_x_labels = False |
show_x_labels = False |
||||||
show_y_labels = False |
show_y_labels = False |
||||||
|
|
||||||
"The font size of the data point labels" |
"The font size of the data point labels" |
||||||
datapoint_font_size = 12 |
datapoint_font_size = 12 |
||||||
|
|
||||||
stylesheet_names = Graph.stylesheet_names + ['pie.css'] |
stylesheet_names = Graph.stylesheet_names + ['pie.css'] |
||||||
|
|
||||||
def add_data(self, data_descriptor): |
def add_data(self, data_descriptor): |
||||||
""" |
""" |
||||||
Add a data set to the graph |
Add a data set to the graph |
||||||
|
|
||||||
>>> graph.add_data({data:[1,2,3,4]}) # doctest: +SKIP |
>>> graph.add_data({data:[1,2,3,4]}) # doctest: +SKIP |
||||||
|
|
||||||
Note that a 'title' key is ignored. |
Note that a 'title' key is ignored. |
||||||
|
|
||||||
Multiple calls to add_data will sum the elements, and the pie will |
Multiple calls to add_data will sum the elements, and the pie will |
||||||
display the aggregated data. e.g. |
display the aggregated data. e.g. |
||||||
|
|
||||||
>>> graph.add_data({data:[1,2,3,4]}) # doctest: +SKIP |
>>> graph.add_data({data:[1,2,3,4]}) # doctest: +SKIP |
||||||
>>> graph.add_data({data:[2,3,5,7]}) # doctest: +SKIP |
>>> graph.add_data({data:[2,3,5,7]}) # doctest: +SKIP |
||||||
|
|
||||||
is the same as: |
is the same as: |
||||||
|
|
||||||
>>> graph.add_data({data:[3,5,8,11]}) # doctest: +SKIP |
>>> graph.add_data({data:[3,5,8,11]}) # doctest: +SKIP |
||||||
|
|
||||||
If data is added of with differing lengths, the corresponding |
If data is added of with differing lengths, the corresponding |
||||||
values will be assumed to be zero. |
values will be assumed to be zero. |
||||||
|
|
||||||
>>> graph.add_data({data:[1,2,3,4]}) # doctest: +SKIP |
>>> graph.add_data({data:[1,2,3,4]}) # doctest: +SKIP |
||||||
>>> graph.add_data({data:[5,7]}) # doctest: +SKIP |
>>> graph.add_data({data:[5,7]}) # doctest: +SKIP |
||||||
|
|
||||||
is the same as: |
is the same as: |
||||||
|
|
||||||
>>> graph.add_data({data:[5,7]}) # doctest: +SKIP |
>>> graph.add_data({data:[5,7]}) # doctest: +SKIP |
||||||
>>> graph.add_data({data:[1,2,3,4]}) # doctest: +SKIP |
>>> graph.add_data({data:[1,2,3,4]}) # doctest: +SKIP |
||||||
|
|
||||||
and |
and |
||||||
|
|
||||||
>>> graph.add_data({data:[6,9,3,4]}) # doctest: +SKIP |
>>> graph.add_data({data:[6,9,3,4]}) # doctest: +SKIP |
||||||
""" |
""" |
||||||
pairs = itertools.izip_longest(self.data, data_descriptor['data']) |
pairs = itertools.izip_longest(self.data, data_descriptor['data']) |
||||||
self.data = list(itertools.starmap(robust_add, pairs)) |
self.data = list(itertools.starmap(robust_add, pairs)) |
||||||
|
|
||||||
def add_defs(self, defs): |
def add_defs(self, defs): |
||||||
"Add svg definitions" |
"Add svg definitions" |
||||||
etree.SubElement( |
etree.SubElement( |
||||||
defs, |
defs, |
||||||
'filter', |
'filter', |
||||||
id='dropshadow', |
id='dropshadow', |
||||||
width='1.2', |
width='1.2', |
||||||
height='1.2', |
height='1.2', |
||||||
) |
) |
||||||
etree.SubElement( |
etree.SubElement( |
||||||
defs, |
defs, |
||||||
'feGaussianBlur', |
'feGaussianBlur', |
||||||
stdDeviation='4', |
stdDeviation='4', |
||||||
result='blur', |
result='blur', |
||||||
) |
) |
||||||
|
|
||||||
def draw_graph(self): |
def draw_graph(self): |
||||||
"Here we don't need the graph (consider refactoring)" |
"Here we don't need the graph (consider refactoring)" |
||||||
pass |
pass |
||||||
|
|
||||||
def get_y_labels(self): |
def get_y_labels(self): |
||||||
"Definitely consider refactoring" |
"Definitely consider refactoring" |
||||||
return [''] |
return [''] |
||||||
|
|
||||||
def get_x_labels(self): |
def get_x_labels(self): |
||||||
"Okay. I'll refactor after this" |
"Okay. I'll refactor after this" |
||||||
return [''] |
return [''] |
||||||
|
|
||||||
def keys(self): |
def keys(self): |
||||||
total = sum(self.data) |
total = sum(self.data) |
||||||
percent_scale = 100.0 / total |
percent_scale = 100.0 / total |
||||||
|
|
||||||
def key(field, value): |
def key(field, value): |
||||||
result = [field] |
result = [field] |
||||||
result.append('[%s]' % value) |
result.append('[%s]' % value) |
||||||
if self.show_key_percent: |
if self.show_key_percent: |
||||||
percent = str(round((value / total * 100))) + '%' |
percent = str(round((value / total * 100))) + '%' |
||||||
result.append(percent) |
result.append(percent) |
||||||
return ' '.join(result) |
return ' '.join(result) |
||||||
return map(key, self.fields, self.data) |
return map(key, self.fields, self.data) |
||||||
|
|
||||||
def draw_data(self): |
def draw_data(self): |
||||||
self.graph = etree.SubElement(self.root, 'g') |
self.graph = etree.SubElement(self.root, 'g') |
||||||
background = etree.SubElement(self.graph, 'g') |
background = etree.SubElement(self.graph, 'g') |
||||||
# midground is somewhere between the background and the foreground |
# midground is somewhere between the background and the foreground |
||||||
midground = etree.SubElement(self.graph, 'g') |
midground = etree.SubElement(self.graph, 'g') |
||||||
|
|
||||||
is_expanded = (self.expanded or self.expand_greatest) |
is_expanded = (self.expanded or self.expand_greatest) |
||||||
diameter = min(self.graph_width, self.graph_height) |
diameter = min(self.graph_width, self.graph_height) |
||||||
# the following assumes int(True)==1 and int(False)==0 |
# the following assumes int(True)==1 and int(False)==0 |
||||||
diameter -= self.expand_gap * int(is_expanded) |
diameter -= self.expand_gap * int(is_expanded) |
||||||
diameter -= self.datapoint_font_size * int(self.show_data_labels) |
diameter -= self.datapoint_font_size * int(self.show_data_labels) |
||||||
diameter -= 10 * int(self.show_shadow) |
diameter -= 10 * int(self.show_shadow) |
||||||
radius = diameter / 2.0 |
radius = diameter / 2.0 |
||||||
|
|
||||||
xoff = (self.width - diameter) / 2 |
xoff = (self.width - diameter) / 2 |
||||||
yoff = (self.height - self.border_bottom - diameter) |
yoff = (self.height - self.border_bottom - diameter) |
||||||
yoff -= 10 * int(self.show_shadow) |
yoff -= 10 * int(self.show_shadow) |
||||||
transform = 'translate(%(xoff)s %(yoff)s)' % vars() |
transform = 'translate(%(xoff)s %(yoff)s)' % vars() |
||||||
self.graph.set('transform', transform) |
self.graph.set('transform', transform) |
||||||
|
|
||||||
wedge_text_pad = 5 |
wedge_text_pad = 5 |
||||||
wedge_text_pad = (20 * int(self.show_percent) * |
wedge_text_pad = (20 * int(self.show_percent) * |
||||||
int(self.show_data_labels)) |
int(self.show_data_labels)) |
||||||
|
|
||||||
total = sum(self.data) |
total = sum(self.data) |
||||||
max_value = max(self.data) |
max_value = max(self.data) |
||||||
|
|
||||||
percent_scale = 100.0 / total |
percent_scale = 100.0 / total |
||||||
|
|
||||||
prev_percent = 0 |
prev_percent = 0 |
||||||
rad_mult = 3.6 * RADIANS |
rad_mult = 3.6 * RADIANS |
||||||
for index, (field, value) in enumerate(zip(self.fields, self.data)): |
for index, (field, value) in enumerate(zip(self.fields, self.data)): |
||||||
percent = percent_scale * value |
percent = percent_scale * value |
||||||
|
|
||||||
radians = prev_percent * rad_mult |
radians = prev_percent * rad_mult |
||||||
x_start = radius + (math.sin(radians) * radius) |
x_start = radius + (math.sin(radians) * radius) |
||||||
y_start = radius - (math.cos(radians) * radius) |
y_start = radius - (math.cos(radians) * radius) |
||||||
radians = (prev_percent + percent) * rad_mult |
radians = (prev_percent + percent) * rad_mult |
||||||
x_end = radius + (math.sin(radians) * radius) |
x_end = radius + (math.sin(radians) * radius) |
||||||
y_end = radius - (math.cos(radians) * radius) |
y_end = radius - (math.cos(radians) * radius) |
||||||
percent_greater_fifty = int(percent >= 50) |
percent_greater_fifty = int(percent >= 50) |
||||||
path = ' '.join(( |
path = ' '.join(( |
||||||
"M%(radius)s,%(radius)s", |
"M%(radius)s,%(radius)s", |
||||||
"L%(x_start)s,%(y_start)s", |
"L%(x_start)s,%(y_start)s", |
||||||
"A%(radius)s,%(radius)s", |
"A%(radius)s,%(radius)s", |
||||||
"0,", |
"0,", |
||||||
"%(percent_greater_fifty)s,1,", |
"%(percent_greater_fifty)s,1,", |
||||||
"%(x_end)s %(y_end)s Z")) |
"%(x_end)s %(y_end)s Z")) |
||||||
path = path % vars() |
path = path % vars() |
||||||
|
|
||||||
wedge = etree.SubElement( |
wedge = etree.SubElement( |
||||||
self.foreground, |
self.foreground, |
||||||
'path', |
'path', |
||||||
{ |
{ |
||||||
'd': path, |
'd': path, |
||||||
'class': 'fill%s' % (index + 1), |
'class': 'fill%s' % (index + 1), |
||||||
} |
} |
||||||
) |
) |
||||||
|
|
||||||
translate = None |
translate = None |
||||||
tx = 0 |
tx = 0 |
||||||
ty = 0 |
ty = 0 |
||||||
half_percent = prev_percent + percent / 2 |
half_percent = prev_percent + percent / 2 |
||||||
radians = half_percent * rad_mult |
radians = half_percent * rad_mult |
||||||
|
|
||||||
if self.show_shadow: |
if self.show_shadow: |
||||||
shadow = etree.SubElement( |
shadow = etree.SubElement( |
||||||
background, |
background, |
||||||
'path', |
'path', |
||||||
d=path, |
d=path, |
||||||
filter='url(#dropshadow)', |
filter='url(#dropshadow)', |
||||||
style='fill: #ccc; stroke: none', |
style='fill: #ccc; stroke: none', |
||||||
) |
) |
||||||
clear = etree.SubElement( |
clear = etree.SubElement( |
||||||
midground, |
midground, |
||||||
'path', |
'path', |
||||||
d=path, |
d=path, |
||||||
# note, this probably only works when the background |
# note, this probably only works when the background |
||||||
# is also #fff |
# is also #fff |
||||||
# consider getting the style from the stylesheet |
# consider getting the style from the stylesheet |
||||||
style="fill:#fff; stroke:none;", |
style="fill:#fff; stroke:none;", |
||||||
) |
) |
||||||
|
|
||||||
if self.expanded or (self.expand_greatest and value == max_value): |
if self.expanded or (self.expand_greatest and value == max_value): |
||||||
tx = (math.sin(radians) * self.expand_gap) |
tx = (math.sin(radians) * self.expand_gap) |
||||||
ty = -(math.cos(radians) * self.expand_gap) |
ty = -(math.cos(radians) * self.expand_gap) |
||||||
translate = "translate(%(tx)s %(ty)s)" % vars() |
translate = "translate(%(tx)s %(ty)s)" % vars() |
||||||
wedge.set('transform', translate) |
wedge.set('transform', translate) |
||||||
clear.set('transform', translate) |
clear.set('transform', translate) |
||||||
|
|
||||||
if self.show_shadow: |
if self.show_shadow: |
||||||
shadow_tx = self.shadow_offset + tx |
shadow_tx = self.shadow_offset + tx |
||||||
shadow_ty = self.shadow_offset + ty |
shadow_ty = self.shadow_offset + ty |
||||||
translate = 'translate(%(shadow_tx)s %(shadow_ty)s)' % vars() |
translate = 'translate(%(shadow_tx)s %(shadow_ty)s)' % vars() |
||||||
shadow.set('transform', translate) |
shadow.set('transform', translate) |
||||||
|
|
||||||
if self.show_data_labels and value != 0: |
if self.show_data_labels and value != 0: |
||||||
label = [] |
label = [] |
||||||
if self.show_key_data_labels: |
if self.show_key_data_labels: |
||||||
label.append(field) |
label.append(field) |
||||||
if self.show_actual_values: |
if self.show_actual_values: |
||||||
label.append('[%s]' % value) |
label.append('[%s]' % value) |
||||||
if self.show_percent: |
if self.show_percent: |
||||||
label.append('%d%%' % round(percent)) |
label.append('%d%%' % round(percent)) |
||||||
label = ' '.join(label) |
label = ' '.join(label) |
||||||
|
|
||||||
msr = math.sin(radians) |
msr = math.sin(radians) |
||||||
mcr = math.cos(radians) |
mcr = math.cos(radians) |
||||||
tx = radius + (msr * radius) |
tx = radius + (msr * radius) |
||||||
ty = radius - (mcr * radius) |
ty = radius - (mcr * radius) |
||||||
|
|
||||||
if self.expanded or ( |
if self.expanded or ( |
||||||
self.expand_greatest and value == max_value): |
self.expand_greatest and value == max_value): |
||||||
tx += (msr * self.expand_gap) |
tx += (msr * self.expand_gap) |
||||||
ty -= (mcr * self.expand_gap) |
ty -= (mcr * self.expand_gap) |
||||||
|
|
||||||
label_node = etree.SubElement( |
label_node = etree.SubElement( |
||||||
self.foreground, |
self.foreground, |
||||||
'text', |
'text', |
||||||
{ |
{ |
||||||
'x': str(tx), |
'x': str(tx), |
||||||
'y': str(ty), |
'y': str(ty), |
||||||
'class': 'dataPointLabel', |
'class': 'dataPointLabel', |
||||||
'style': 'stroke: #fff; stroke-width: 2;' |
'style': 'stroke: #fff; stroke-width: 2;' |
||||||
} |
} |
||||||
) |
) |
||||||
label_node.text = label |
label_node.text = label |
||||||
|
|
||||||
label_node = etree.SubElement( |
label_node = etree.SubElement( |
||||||
self.foreground, |
self.foreground, |
||||||
'text', |
'text', |
||||||
{ |
{ |
||||||
'x': str(tx), |
'x': str(tx), |
||||||
'y': str(ty), |
'y': str(ty), |
||||||
'class': 'dataPointLabel', |
'class': 'dataPointLabel', |
||||||
} |
} |
||||||
) |
) |
||||||
label_node.text = label |
label_node.text = label |
||||||
|
|
||||||
prev_percent += percent |
prev_percent += percent |
||||||
|
|
||||||
def round(self, val, to): |
def round(self, val, to): |
||||||
return round(val, to) |
return round(val, to) |
||||||
|
@ -1,193 +1,193 @@ |
|||||||
/* |
/* |
||||||
$Id$ |
$Id$ |
||||||
|
|
||||||
default line styles |
default line styles |
||||||
*/ |
*/ |
||||||
.line1{ |
.line1{ |
||||||
fill: none; |
fill: none; |
||||||
stroke: #ff0000; |
stroke: #ff0000; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.line2{ |
.line2{ |
||||||
fill: none; |
fill: none; |
||||||
stroke: #0000ff; |
stroke: #0000ff; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.line3{ |
.line3{ |
||||||
fill: none; |
fill: none; |
||||||
stroke: #00ff00; |
stroke: #00ff00; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.line4{ |
.line4{ |
||||||
fill: none; |
fill: none; |
||||||
stroke: #ffcc00; |
stroke: #ffcc00; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.line5{ |
.line5{ |
||||||
fill: none; |
fill: none; |
||||||
stroke: #00ccff; |
stroke: #00ccff; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.line6{ |
.line6{ |
||||||
fill: none; |
fill: none; |
||||||
stroke: #ff00ff; |
stroke: #ff00ff; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.line7{ |
.line7{ |
||||||
fill: none; |
fill: none; |
||||||
stroke: #00ffff; |
stroke: #00ffff; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.line8{ |
.line8{ |
||||||
fill: none; |
fill: none; |
||||||
stroke: #ffff00; |
stroke: #ffff00; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.line9{ |
.line9{ |
||||||
fill: none; |
fill: none; |
||||||
stroke: #cc6666; |
stroke: #cc6666; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.line10{ |
.line10{ |
||||||
fill: none; |
fill: none; |
||||||
stroke: #663399; |
stroke: #663399; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.line11{ |
.line11{ |
||||||
fill: none; |
fill: none; |
||||||
stroke: #339900; |
stroke: #339900; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.line12{ |
.line12{ |
||||||
fill: none; |
fill: none; |
||||||
stroke: #9966FF; |
stroke: #9966FF; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
/* default fill styles */ |
/* default fill styles */ |
||||||
.fill1{ |
.fill1{ |
||||||
fill: #cc0000; |
fill: #cc0000; |
||||||
fill-opacity: 0.2; |
fill-opacity: 0.2; |
||||||
stroke: none; |
stroke: none; |
||||||
} |
} |
||||||
.fill2{ |
.fill2{ |
||||||
fill: #0000cc; |
fill: #0000cc; |
||||||
fill-opacity: 0.2; |
fill-opacity: 0.2; |
||||||
stroke: none; |
stroke: none; |
||||||
} |
} |
||||||
.fill3{ |
.fill3{ |
||||||
fill: #00cc00; |
fill: #00cc00; |
||||||
fill-opacity: 0.2; |
fill-opacity: 0.2; |
||||||
stroke: none; |
stroke: none; |
||||||
} |
} |
||||||
.fill4{ |
.fill4{ |
||||||
fill: #ffcc00; |
fill: #ffcc00; |
||||||
fill-opacity: 0.2; |
fill-opacity: 0.2; |
||||||
stroke: none; |
stroke: none; |
||||||
} |
} |
||||||
.fill5{ |
.fill5{ |
||||||
fill: #00ccff; |
fill: #00ccff; |
||||||
fill-opacity: 0.2; |
fill-opacity: 0.2; |
||||||
stroke: none; |
stroke: none; |
||||||
} |
} |
||||||
.fill6{ |
.fill6{ |
||||||
fill: #ff00ff; |
fill: #ff00ff; |
||||||
fill-opacity: 0.2; |
fill-opacity: 0.2; |
||||||
stroke: none; |
stroke: none; |
||||||
} |
} |
||||||
.fill7{ |
.fill7{ |
||||||
fill: #00ffff; |
fill: #00ffff; |
||||||
fill-opacity: 0.2; |
fill-opacity: 0.2; |
||||||
stroke: none; |
stroke: none; |
||||||
} |
} |
||||||
.fill8{ |
.fill8{ |
||||||
fill: #ffff00; |
fill: #ffff00; |
||||||
fill-opacity: 0.2; |
fill-opacity: 0.2; |
||||||
stroke: none; |
stroke: none; |
||||||
} |
} |
||||||
.fill9{ |
.fill9{ |
||||||
fill: #cc6666; |
fill: #cc6666; |
||||||
fill-opacity: 0.2; |
fill-opacity: 0.2; |
||||||
stroke: none; |
stroke: none; |
||||||
} |
} |
||||||
.fill10{ |
.fill10{ |
||||||
fill: #663399; |
fill: #663399; |
||||||
fill-opacity: 0.2; |
fill-opacity: 0.2; |
||||||
stroke: none; |
stroke: none; |
||||||
} |
} |
||||||
.fill11{ |
.fill11{ |
||||||
fill: #339900; |
fill: #339900; |
||||||
fill-opacity: 0.2; |
fill-opacity: 0.2; |
||||||
stroke: none; |
stroke: none; |
||||||
} |
} |
||||||
.fill12{ |
.fill12{ |
||||||
fill: #9966FF; |
fill: #9966FF; |
||||||
fill-opacity: 0.2; |
fill-opacity: 0.2; |
||||||
stroke: none; |
stroke: none; |
||||||
} |
} |
||||||
/* default line styles */ |
/* default line styles */ |
||||||
.key1,.dataPoint1{ |
.key1,.dataPoint1{ |
||||||
fill: #ff0000; |
fill: #ff0000; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key2,.dataPoint2{ |
.key2,.dataPoint2{ |
||||||
fill: #0000ff; |
fill: #0000ff; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key3,.dataPoint3{ |
.key3,.dataPoint3{ |
||||||
fill: #00ff00; |
fill: #00ff00; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key4,.dataPoint4{ |
.key4,.dataPoint4{ |
||||||
fill: #ffcc00; |
fill: #ffcc00; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key5,.dataPoint5{ |
.key5,.dataPoint5{ |
||||||
fill: #00ccff; |
fill: #00ccff; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key6,.dataPoint6{ |
.key6,.dataPoint6{ |
||||||
fill: #ff00ff; |
fill: #ff00ff; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key7,.dataPoint7{ |
.key7,.dataPoint7{ |
||||||
fill: #00ffff; |
fill: #00ffff; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key8,.dataPoint8{ |
.key8,.dataPoint8{ |
||||||
fill: #ffff00; |
fill: #ffff00; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key9,.dataPoint9{ |
.key9,.dataPoint9{ |
||||||
fill: #cc6666; |
fill: #cc6666; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key10,.dataPoint10{ |
.key10,.dataPoint10{ |
||||||
fill: #663399; |
fill: #663399; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key11,.dataPoint11{ |
.key11,.dataPoint11{ |
||||||
fill: #339900; |
fill: #339900; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.key12,.dataPoint12{ |
.key12,.dataPoint12{ |
||||||
fill: #9966FF; |
fill: #9966FF; |
||||||
stroke: none; |
stroke: none; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
} |
} |
||||||
.constantLine{ |
.constantLine{ |
||||||
color: navy; |
color: navy; |
||||||
stroke: navy; |
stroke: navy; |
||||||
stroke-width: 1px; |
stroke-width: 1px; |
||||||
stroke-dasharray: 9,1,1; |
stroke-dasharray: 9,1,1; |
||||||
} |
} |
||||||
|
@ -1,363 +1,363 @@ |
|||||||
# -*- coding: utf-8 -*- |
# -*- coding: utf-8 -*- |
||||||
|
|
||||||
"plot.py" |
"plot.py" |
||||||
|
|
||||||
import sys |
import sys |
||||||
from itertools import izip, count, chain |
from itertools import izip, count, chain |
||||||
from lxml import etree |
from lxml import etree |
||||||
|
|
||||||
from pygal.graph import Graph |
from pygal.graph import Graph |
||||||
|
|
||||||
from .util import float_range |
from .util import float_range |
||||||
|
|
||||||
|
|
||||||
def get_pairs(i): |
def get_pairs(i): |
||||||
i = iter(i) |
i = iter(i) |
||||||
while True: |
while True: |
||||||
yield i.next(), i.next() |
yield i.next(), i.next() |
||||||
|
|
||||||
# I'm not sure how this is more beautiful than ugly. |
# I'm not sure how this is more beautiful than ugly. |
||||||
if sys.version >= '3': |
if sys.version >= '3': |
||||||
def apply(func): |
def apply(func): |
||||||
return func() |
return func() |
||||||
|
|
||||||
|
|
||||||
class Plot(Graph): |
class Plot(Graph): |
||||||
"""=== For creating SVG plots of scalar data |
"""=== For creating SVG plots of scalar data |
||||||
|
|
||||||
= Synopsis |
= Synopsis |
||||||
|
|
||||||
require 'SVG/Graph/Plot' |
require 'SVG/Graph/Plot' |
||||||
|
|
||||||
# Data sets are x,y pairs |
# Data sets are x,y pairs |
||||||
# Note that multiple data sets can differ in length, and that the |
# 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 |
# data in the datasets needn't be in order; they will be ordered |
||||||
# by the plot along the X-axis. |
# by the plot along the X-axis. |
||||||
projection = [ |
projection = [ |
||||||
6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13, |
6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13, |
||||||
7, 9 |
7, 9 |
||||||
] |
] |
||||||
actual = [ |
actual = [ |
||||||
0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12, |
0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12, |
||||||
15, 6, 4, 17, 2, 12 |
15, 6, 4, 17, 2, 12 |
||||||
] |
] |
||||||
|
|
||||||
graph = SVG::Graph::Plot.new({ |
graph = SVG::Graph::Plot.new({ |
||||||
:height => 500, |
:height => 500, |
||||||
:width => 300, |
:width => 300, |
||||||
:key => true, |
:key => true, |
||||||
:scale_x_integers => true, |
:scale_x_integers => true, |
||||||
:scale_y_integerrs => true, |
:scale_y_integerrs => true, |
||||||
}) |
}) |
||||||
|
|
||||||
graph.add_data({ |
graph.add_data({ |
||||||
:data => projection |
:data => projection |
||||||
:title => 'Projected', |
:title => 'Projected', |
||||||
}) |
}) |
||||||
|
|
||||||
graph.add_data({ |
graph.add_data({ |
||||||
:data => actual, |
:data => actual, |
||||||
:title => 'Actual', |
:title => 'Actual', |
||||||
}) |
}) |
||||||
|
|
||||||
print graph.burn() |
print graph.burn() |
||||||
|
|
||||||
= Description |
= Description |
||||||
|
|
||||||
Produces a graph of scalar data. |
Produces a graph of scalar data. |
||||||
|
|
||||||
This object aims to allow you to easily create high quality |
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 |
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 |
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 |
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, |
generated - with or without a key, data elements at each point, title, |
||||||
subtitle etc. |
subtitle etc. |
||||||
|
|
||||||
= Examples |
= Examples |
||||||
|
|
||||||
http://www.germane-software/repositories/public/SVG/test/plot.rb |
http://www.germane-software/repositories/public/SVG/test/plot.rb |
||||||
|
|
||||||
= Notes |
= Notes |
||||||
|
|
||||||
The default stylesheet handles upto 10 data sets, if you |
The default stylesheet handles upto 10 data sets, if you |
||||||
use more you must create your own stylesheet and add the |
use more you must create your own stylesheet and add the |
||||||
additional settings for the extra data sets. You will know |
additional settings for the extra data sets. You will know |
||||||
if you go over 10 data sets as they will have no style and |
if you go over 10 data sets as they will have no style and |
||||||
be in black. |
be in black. |
||||||
|
|
||||||
Unlike the other types of charts, data sets must contain x,y pairs: |
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] # A data set with 1 point: (1,2) |
||||||
[1,2, 5,6] # A data set with 2 points: (1,2) and (5,6) |
[1,2, 5,6] # A data set with 2 points: (1,2) and (5,6) |
||||||
|
|
||||||
= See also |
= See also |
||||||
|
|
||||||
* SVG::Graph::Graph |
* SVG::Graph::Graph |
||||||
* SVG::Graph::BarHorizontal |
* SVG::Graph::BarHorizontal |
||||||
* SVG::Graph::Bar |
* SVG::Graph::Bar |
||||||
* SVG::Graph::Line |
* SVG::Graph::Line |
||||||
* SVG::Graph::Pie |
* SVG::Graph::Pie |
||||||
* SVG::Graph::TimeSeries |
* SVG::Graph::TimeSeries |
||||||
|
|
||||||
== Author |
== Author |
||||||
|
|
||||||
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> |
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> |
||||||
|
|
||||||
Copyright 2004 Sean E. Russell |
Copyright 2004 Sean E. Russell |
||||||
This software is available under the Ruby license[LICENSE.txt]""" |
This software is available under the Ruby license[LICENSE.txt]""" |
||||||
|
|
||||||
top_align = right_align = top_font = right_font = 1 |
top_align = right_align = top_font = right_font = 1 |
||||||
|
|
||||||
"""Determines the scaling for the Y axis divisions. |
"""Determines the scaling for the Y axis divisions. |
||||||
|
|
||||||
graph.scale_y_divisions = 0.5 |
graph.scale_y_divisions = 0.5 |
||||||
|
|
||||||
would cause the graph to attempt to generate labels stepped by 0.5; EG: |
would cause the graph to attempt to generate labels stepped by 0.5; EG: |
||||||
0, 0.5, 1, 1.5, 2, ...""" |
0, 0.5, 1, 1.5, 2, ...""" |
||||||
scale_y_divisions = None |
scale_y_divisions = None |
||||||
"Make the X axis labels integers" |
"Make the X axis labels integers" |
||||||
scale_x_integers = False |
scale_x_integers = False |
||||||
"Make the Y axis labels integers" |
"Make the Y axis labels integers" |
||||||
scale_y_integers = False |
scale_y_integers = False |
||||||
"Fill the area under the line" |
"Fill the area under the line" |
||||||
area_fill = False |
area_fill = False |
||||||
"""Show a small circle on the graph where the line |
"""Show a small circle on the graph where the line |
||||||
goes from one point to the next.""" |
goes from one point to the next.""" |
||||||
show_data_points = True |
show_data_points = True |
||||||
"Indicate whether the lines should be drawn between points" |
"Indicate whether the lines should be drawn between points" |
||||||
draw_lines_between_points = True |
draw_lines_between_points = True |
||||||
"Set the minimum value of the X axis" |
"Set the minimum value of the X axis" |
||||||
min_x_value = None |
min_x_value = None |
||||||
"Set the minimum value of the Y axis" |
"Set the minimum value of the Y axis" |
||||||
min_y_value = None |
min_y_value = None |
||||||
"Set the maximum value of the X axis" |
"Set the maximum value of the X axis" |
||||||
max_x_value = None |
max_x_value = None |
||||||
"Set the maximum value of the Y axis" |
"Set the maximum value of the Y axis" |
||||||
max_y_value = None |
max_y_value = None |
||||||
|
|
||||||
stacked = False |
stacked = False |
||||||
|
|
||||||
stylesheet_names = Graph.stylesheet_names + ['plot.css'] |
stylesheet_names = Graph.stylesheet_names + ['plot.css'] |
||||||
|
|
||||||
@apply |
@apply |
||||||
def scale_x_divisions(): |
def scale_x_divisions(): |
||||||
doc = """Determines the scaling for the X axis divisions. |
doc = """Determines the scaling for the X axis divisions. |
||||||
|
|
||||||
graph.scale_x_divisions = 2 |
graph.scale_x_divisions = 2 |
||||||
|
|
||||||
would cause the graph to attempt |
would cause the graph to attempt |
||||||
to generate labels stepped by 2; EG: |
to generate labels stepped by 2; EG: |
||||||
0,2,4,6,8...""" |
0,2,4,6,8...""" |
||||||
|
|
||||||
def fget(self): |
def fget(self): |
||||||
return getattr(self, '_scale_x_divisions', None) |
return getattr(self, '_scale_x_divisions', None) |
||||||
|
|
||||||
def fset(self, val): |
def fset(self, val): |
||||||
self._scale_x_divisions = val |
self._scale_x_divisions = val |
||||||
return property(**locals()) |
return property(**locals()) |
||||||
|
|
||||||
def validate_data(self, data): |
def validate_data(self, data): |
||||||
if len(data['data']) % 2 != 0: |
if len(data['data']) % 2 != 0: |
||||||
raise ValueError( |
raise ValueError( |
||||||
"Expecting x,y pairs for data points for %s." % |
"Expecting x,y pairs for data points for %s." % |
||||||
self.__class__.__name__) |
self.__class__.__name__) |
||||||
|
|
||||||
def process_data(self, data): |
def process_data(self, data): |
||||||
pairs = list(get_pairs(data['data'])) |
pairs = list(get_pairs(data['data'])) |
||||||
pairs.sort() |
pairs.sort() |
||||||
data['data'] = zip(*pairs) |
data['data'] = zip(*pairs) |
||||||
|
|
||||||
def calculate_left_margin(self): |
def calculate_left_margin(self): |
||||||
super(Plot, self).calculate_left_margin() |
super(Plot, self).calculate_left_margin() |
||||||
label_left = len(str( |
label_left = len(str( |
||||||
self.get_x_labels()[0])) / 2 * self.font_size * 0.6 |
self.get_x_labels()[0])) / 2 * self.font_size * 0.6 |
||||||
self.border_left = max(label_left, self.border_left) |
self.border_left = max(label_left, self.border_left) |
||||||
|
|
||||||
def calculate_right_margin(self): |
def calculate_right_margin(self): |
||||||
super(Plot, self).calculate_right_margin() |
super(Plot, self).calculate_right_margin() |
||||||
label_right = len(str( |
label_right = len(str( |
||||||
self.get_x_labels()[-1])) / 2 * self.font_size * 0.6 |
self.get_x_labels()[-1])) / 2 * self.font_size * 0.6 |
||||||
self.border_right = max(label_right, self.border_right) |
self.border_right = max(label_right, self.border_right) |
||||||
|
|
||||||
def data_max(self, axis): |
def data_max(self, axis): |
||||||
data_index = getattr(self, '%s_data_index' % axis) |
data_index = getattr(self, '%s_data_index' % axis) |
||||||
max_value = max(chain( |
max_value = max(chain( |
||||||
*map(lambda set: set['data'][data_index], self.data))) |
*map(lambda set: set['data'][data_index], self.data))) |
||||||
# above is same as |
# above is same as |
||||||
#max_value = max(map(lambda set: |
#max_value = max(map(lambda set: |
||||||
# max(set['data'][data_index]), self.data)) |
# max(set['data'][data_index]), self.data)) |
||||||
spec_max = getattr(self, 'max_%s_value' % axis) |
spec_max = getattr(self, 'max_%s_value' % axis) |
||||||
# Python 3 doesn't allow comparing None to int, so use -∞ |
# Python 3 doesn't allow comparing None to int, so use -∞ |
||||||
if spec_max is None: |
if spec_max is None: |
||||||
spec_max = float('-Inf') |
spec_max = float('-Inf') |
||||||
max_value = max(max_value, spec_max) |
max_value = max(max_value, spec_max) |
||||||
return max_value |
return max_value |
||||||
|
|
||||||
def data_min(self, axis): |
def data_min(self, axis): |
||||||
data_index = getattr(self, '%s_data_index' % axis) |
data_index = getattr(self, '%s_data_index' % axis) |
||||||
min_value = min(chain( |
min_value = min(chain( |
||||||
*map(lambda set: set['data'][data_index], self.data))) |
*map(lambda set: set['data'][data_index], self.data))) |
||||||
spec_min = getattr(self, 'min_%s_value' % axis) |
spec_min = getattr(self, 'min_%s_value' % axis) |
||||||
if spec_min is not None: |
if spec_min is not None: |
||||||
min_value = min(min_value, spec_min) |
min_value = min(min_value, spec_min) |
||||||
return min_value |
return min_value |
||||||
|
|
||||||
x_data_index = 0 |
x_data_index = 0 |
||||||
y_data_index = 1 |
y_data_index = 1 |
||||||
|
|
||||||
def data_range(self, axis): |
def data_range(self, axis): |
||||||
side = {'x': 'right', 'y': 'top'}[axis] |
side = {'x': 'right', 'y': 'top'}[axis] |
||||||
|
|
||||||
min_value = self.data_min(axis) |
min_value = self.data_min(axis) |
||||||
max_value = self.data_max(axis) |
max_value = self.data_max(axis) |
||||||
range = max_value - min_value |
range = max_value - min_value |
||||||
|
|
||||||
side_pad = range / 20.0 or 10 |
side_pad = range / 20.0 or 10 |
||||||
scale_range = (max_value + side_pad) - min_value |
scale_range = (max_value + side_pad) - min_value |
||||||
|
|
||||||
scale_division = getattr( |
scale_division = getattr( |
||||||
self, 'scale_%s_divisions' % axis) or (scale_range / 10.0) |
self, 'scale_%s_divisions' % axis) or (scale_range / 10.0) |
||||||
|
|
||||||
if getattr(self, 'scale_%s_integers' % axis): |
if getattr(self, 'scale_%s_integers' % axis): |
||||||
scale_division = round(scale_division) or 1 |
scale_division = round(scale_division) or 1 |
||||||
|
|
||||||
return min_value, max_value, scale_division |
return min_value, max_value, scale_division |
||||||
|
|
||||||
def x_range(self): |
def x_range(self): |
||||||
return self.data_range('x') |
return self.data_range('x') |
||||||
|
|
||||||
def y_range(self): |
def y_range(self): |
||||||
return self.data_range('y') |
return self.data_range('y') |
||||||
|
|
||||||
def get_data_values(self, axis): |
def get_data_values(self, axis): |
||||||
min_value, max_value, scale_division = self.data_range(axis) |
min_value, max_value, scale_division = self.data_range(axis) |
||||||
return tuple(float_range(*self.data_range(axis))) |
return tuple(float_range(*self.data_range(axis))) |
||||||
|
|
||||||
def get_x_values(self): |
def get_x_values(self): |
||||||
return self.get_data_values('x') |
return self.get_data_values('x') |
||||||
|
|
||||||
def get_y_values(self): |
def get_y_values(self): |
||||||
return self.get_data_values('y') |
return self.get_data_values('y') |
||||||
|
|
||||||
def get_x_labels(self): |
def get_x_labels(self): |
||||||
return map(str, self.get_x_values()) |
return map(str, self.get_x_values()) |
||||||
|
|
||||||
def get_y_labels(self): |
def get_y_labels(self): |
||||||
return map(str, self.get_y_values()) |
return map(str, self.get_y_values()) |
||||||
|
|
||||||
def field_size(self, axis): |
def field_size(self, axis): |
||||||
size = {'x': 'width', 'y': 'height'}[axis] |
size = {'x': 'width', 'y': 'height'}[axis] |
||||||
side = {'x': 'right', 'y': 'top'}[axis] |
side = {'x': 'right', 'y': 'top'}[axis] |
||||||
values = getattr(self, 'get_%s_values' % axis)() |
values = getattr(self, 'get_%s_values' % axis)() |
||||||
max_d = self.data_max(axis) |
max_d = self.data_max(axis) |
||||||
dx = ( |
dx = ( |
||||||
float(max_d - values[-1]) / (values[-1] - values[-2]) |
float(max_d - values[-1]) / (values[-1] - values[-2]) |
||||||
if len(values) > 1 else max_d |
if len(values) > 1 else max_d |
||||||
) |
) |
||||||
graph_size = getattr(self, 'graph_%s' % size) |
graph_size = getattr(self, 'graph_%s' % size) |
||||||
side_font = getattr(self, '%s_font' % side) |
side_font = getattr(self, '%s_font' % side) |
||||||
side_align = getattr(self, '%s_align' % side) |
side_align = getattr(self, '%s_align' % side) |
||||||
result = ((float(graph_size) - self.font_size * 2 * side_font) / |
result = ((float(graph_size) - self.font_size * 2 * side_font) / |
||||||
(len(values) + dx - side_align)) |
(len(values) + dx - side_align)) |
||||||
return result |
return result |
||||||
|
|
||||||
def field_width(self): |
def field_width(self): |
||||||
return self.field_size('x') |
return self.field_size('x') |
||||||
|
|
||||||
def field_height(self): |
def field_height(self): |
||||||
return self.field_size('y') |
return self.field_size('y') |
||||||
|
|
||||||
def draw_data(self): |
def draw_data(self): |
||||||
self.load_transform_parameters() |
self.load_transform_parameters() |
||||||
for line, data in izip(count(1), self.data): |
for line, data in izip(count(1), self.data): |
||||||
x_start, y_start = self.transform_output_coordinates( |
x_start, y_start = self.transform_output_coordinates( |
||||||
(data['data'][self.x_data_index][0], |
(data['data'][self.x_data_index][0], |
||||||
data['data'][self.y_data_index][0]) |
data['data'][self.y_data_index][0]) |
||||||
) |
) |
||||||
data_points = zip(*data['data']) |
data_points = zip(*data['data']) |
||||||
graph_points = self.get_graph_points(data_points) |
graph_points = self.get_graph_points(data_points) |
||||||
lpath = self.get_lpath(graph_points) |
lpath = self.get_lpath(graph_points) |
||||||
if self.area_fill: |
if self.area_fill: |
||||||
graph_height = self.graph_height |
graph_height = self.graph_height |
||||||
path = etree.SubElement(self.graph, 'path', { |
path = etree.SubElement(self.graph, 'path', { |
||||||
'd': 'M%(x_start)f %(graph_height)f' |
'd': 'M%(x_start)f %(graph_height)f' |
||||||
' %(lpath)s V%(graph_height)f Z' % vars(), |
' %(lpath)s V%(graph_height)f Z' % vars(), |
||||||
'class': 'fill%(line)d' % vars()}) |
'class': 'fill%(line)d' % vars()}) |
||||||
if self.draw_lines_between_points: |
if self.draw_lines_between_points: |
||||||
path = etree.SubElement(self.graph, 'path', { |
path = etree.SubElement(self.graph, 'path', { |
||||||
'd': 'M%(x_start)f %(y_start)f %(lpath)s' % vars(), |
'd': 'M%(x_start)f %(y_start)f %(lpath)s' % vars(), |
||||||
'class': 'line%(line)d' % vars()}) |
'class': 'line%(line)d' % vars()}) |
||||||
self.draw_data_points(line, data_points, graph_points) |
self.draw_data_points(line, data_points, graph_points) |
||||||
self._draw_constant_lines() |
self._draw_constant_lines() |
||||||
del self.__transform_parameters |
del self.__transform_parameters |
||||||
|
|
||||||
def add_constant_line(self, value, label=None, style=None): |
def add_constant_line(self, value, label=None, style=None): |
||||||
self.constant_lines = getattr(self, 'constant_lines', []) |
self.constant_lines = getattr(self, 'constant_lines', []) |
||||||
self.constant_lines.append((value, label, style)) |
self.constant_lines.append((value, label, style)) |
||||||
|
|
||||||
def _draw_constant_lines(self): |
def _draw_constant_lines(self): |
||||||
if hasattr(self, 'constant_lines'): |
if hasattr(self, 'constant_lines'): |
||||||
map(self.__draw_constant_line, self.constant_lines) |
map(self.__draw_constant_line, self.constant_lines) |
||||||
|
|
||||||
def __draw_constant_line(self, value_label_style): |
def __draw_constant_line(self, value_label_style): |
||||||
"Draw a constant line on the y-axis with the label" |
"Draw a constant line on the y-axis with the label" |
||||||
value, label, style = value_label_style |
value, label, style = value_label_style |
||||||
start = self.transform_output_coordinates((0, value))[1] |
start = self.transform_output_coordinates((0, value))[1] |
||||||
stop = self.graph_width |
stop = self.graph_width |
||||||
path = etree.SubElement(self.graph, 'path', { |
path = etree.SubElement(self.graph, 'path', { |
||||||
'd': 'M 0 %(start)s h%(stop)s' % vars(), |
'd': 'M 0 %(start)s h%(stop)s' % vars(), |
||||||
'class': 'constantLine'}) |
'class': 'constantLine'}) |
||||||
if style: |
if style: |
||||||
path.set('style', style) |
path.set('style', style) |
||||||
text = etree.SubElement(self.graph, 'text', { |
text = etree.SubElement(self.graph, 'text', { |
||||||
'x': str(2), |
'x': str(2), |
||||||
'y': str(start - 2), |
'y': str(start - 2), |
||||||
'class': 'constantLine'}) |
'class': 'constantLine'}) |
||||||
text.text = label |
text.text = label |
||||||
|
|
||||||
def load_transform_parameters(self): |
def load_transform_parameters(self): |
||||||
"Cache the parameters necessary to transform x & y coordinates" |
"Cache the parameters necessary to transform x & y coordinates" |
||||||
x_min, x_max, x_div = self.x_range() |
x_min, x_max, x_div = self.x_range() |
||||||
y_min, y_max, y_div = self.y_range() |
y_min, y_max, y_div = self.y_range() |
||||||
x_step = ((float(self.graph_width) - self.font_size * 2) / |
x_step = ((float(self.graph_width) - self.font_size * 2) / |
||||||
(x_max - x_min)) |
(x_max - x_min)) |
||||||
y_step = ((float(self.graph_height) - self.font_size * 2) / |
y_step = ((float(self.graph_height) - self.font_size * 2) / |
||||||
(y_max - y_min)) |
(y_max - y_min)) |
||||||
self.__transform_parameters = dict(vars()) |
self.__transform_parameters = dict(vars()) |
||||||
del self.__transform_parameters['self'] |
del self.__transform_parameters['self'] |
||||||
|
|
||||||
def get_graph_points(self, data_points): |
def get_graph_points(self, data_points): |
||||||
return map(self.transform_output_coordinates, data_points) |
return map(self.transform_output_coordinates, data_points) |
||||||
|
|
||||||
def get_lpath(self, points): |
def get_lpath(self, points): |
||||||
points = map(lambda p: "%f %f" % p, points) |
points = map(lambda p: "%f %f" % p, points) |
||||||
return 'L' + ' '.join(points) |
return 'L' + ' '.join(points) |
||||||
|
|
||||||
def transform_output_coordinates(self, (x, y)): |
def transform_output_coordinates(self, (x, y)): |
||||||
x_min = self.__transform_parameters['x_min'] |
x_min = self.__transform_parameters['x_min'] |
||||||
x_step = self.__transform_parameters['x_step'] |
x_step = self.__transform_parameters['x_step'] |
||||||
y_min = self.__transform_parameters['y_min'] |
y_min = self.__transform_parameters['y_min'] |
||||||
y_step = self.__transform_parameters['y_step'] |
y_step = self.__transform_parameters['y_step'] |
||||||
#locals().update(self.__transform_parameters) |
#locals().update(self.__transform_parameters) |
||||||
#vars().update(self.__transform_parameters) |
#vars().update(self.__transform_parameters) |
||||||
x = (x - x_min) * x_step |
x = (x - x_min) * x_step |
||||||
y = self.graph_height - (y - y_min) * y_step |
y = self.graph_height - (y - y_min) * y_step |
||||||
return x, y |
return x, y |
||||||
|
|
||||||
def draw_data_points(self, line, data_points, graph_points): |
def draw_data_points(self, line, data_points, graph_points): |
||||||
if not self.show_data_points and not self.show_data_values: |
if not self.show_data_points and not self.show_data_values: |
||||||
return |
return |
||||||
|
|
||||||
for ((dx, dy), (gx, gy)) in izip(data_points, graph_points): |
for ((dx, dy), (gx, gy)) in izip(data_points, graph_points): |
||||||
if self.show_data_points: |
if self.show_data_points: |
||||||
etree.SubElement(self.graph, 'circle', { |
etree.SubElement(self.graph, 'circle', { |
||||||
'cx': str(gx), |
'cx': str(gx), |
||||||
'cy': str(gy), |
'cy': str(gy), |
||||||
'r': '2.5', |
'r': '2.5', |
||||||
'class': 'dataPoint%(line)s' % vars()}) |
'class': 'dataPoint%(line)s' % vars()}) |
||||||
if self.show_data_values: |
if self.show_data_values: |
||||||
self.add_popup(gx, gy, self.format(dx, dy)) |
self.add_popup(gx, gy, self.format(dx, dy)) |
||||||
self.make_datapoint_text(gx, gy - 6, dy) |
self.make_datapoint_text(gx, gy - 6, dy) |
||||||
|
|
||||||
def format(self, x, y): |
def format(self, x, y): |
||||||
return '(%0.2f, %0.2f)' % (x, y) |
return '(%0.2f, %0.2f)' % (x, y) |
||||||
|
@ -1,313 +1,313 @@ |
|||||||
#!python |
#!python |
||||||
import re |
import re |
||||||
|
|
||||||
from dateutil.parser import parse |
from dateutil.parser import parse |
||||||
from dateutil.relativedelta import relativedelta |
from dateutil.relativedelta import relativedelta |
||||||
from lxml import etree |
from lxml import etree |
||||||
|
|
||||||
from pygal.graph import Graph |
from pygal.graph import Graph |
||||||
from util import grouper, date_range, divide_timedelta_float, TimeScale |
from util import grouper, date_range, divide_timedelta_float, TimeScale |
||||||
|
|
||||||
__all__ = ('Schedule') |
__all__ = ('Schedule') |
||||||
|
|
||||||
|
|
||||||
class Schedule(Graph): |
class Schedule(Graph): |
||||||
""" |
""" |
||||||
# === For creating SVG plots of scalar temporal data |
# === For creating SVG plots of scalar temporal data |
||||||
|
|
||||||
= Synopsis |
= Synopsis |
||||||
|
|
||||||
require 'SVG/Graph/Schedule' |
require 'SVG/Graph/Schedule' |
||||||
|
|
||||||
# Data sets are label, start, end tripples. |
# Data sets are label, start, end tripples. |
||||||
data1 = [ |
data1 = [ |
||||||
"Housesitting", "6/17/04", "6/19/04", |
"Housesitting", "6/17/04", "6/19/04", |
||||||
"Summer Session", "6/15/04", "8/15/04", |
"Summer Session", "6/15/04", "8/15/04", |
||||||
] |
] |
||||||
|
|
||||||
graph = SVG::Graph::Schedule.new( { |
graph = SVG::Graph::Schedule.new( { |
||||||
:width => 640, |
:width => 640, |
||||||
:height => 480, |
:height => 480, |
||||||
:graph_title => title, |
:graph_title => title, |
||||||
:show_graph_title => true, |
:show_graph_title => true, |
||||||
:no_css => true, |
:no_css => true, |
||||||
:scale_x_integers => true, |
:scale_x_integers => true, |
||||||
:scale_y_integers => true, |
:scale_y_integers => true, |
||||||
:min_x_value => 0, |
:min_x_value => 0, |
||||||
:min_y_value => 0, |
:min_y_value => 0, |
||||||
:show_data_labels => true, |
:show_data_labels => true, |
||||||
:show_x_guidelines => true, |
:show_x_guidelines => true, |
||||||
:show_x_title => true, |
:show_x_title => true, |
||||||
:x_title => "Time", |
:x_title => "Time", |
||||||
:stagger_x_labels => true, |
:stagger_x_labels => true, |
||||||
:stagger_y_labels => true, |
:stagger_y_labels => true, |
||||||
:x_label_format => "%m/%d/%y", |
:x_label_format => "%m/%d/%y", |
||||||
}) |
}) |
||||||
|
|
||||||
graph.add_data({ |
graph.add_data({ |
||||||
:data => data1, |
:data => data1, |
||||||
:title => 'Data', |
:title => 'Data', |
||||||
}) |
}) |
||||||
|
|
||||||
print graph.burn() |
print graph.burn() |
||||||
|
|
||||||
= Description |
= Description |
||||||
|
|
||||||
Produces a graph of temporal scalar data. |
Produces a graph of temporal scalar data. |
||||||
|
|
||||||
= Examples |
= Examples |
||||||
|
|
||||||
http://www.germane-software/repositories/public/SVG/test/schedule.rb |
http://www.germane-software/repositories/public/SVG/test/schedule.rb |
||||||
|
|
||||||
= Notes |
= Notes |
||||||
|
|
||||||
The default stylesheet handles upto 10 data sets, if you |
The default stylesheet handles upto 10 data sets, if you |
||||||
use more you must create your own stylesheet and add the |
use more you must create your own stylesheet and add the |
||||||
additional settings for the extra data sets. You will know |
additional settings for the extra data sets. You will know |
||||||
if you go over 10 data sets as they will have no style and |
if you go over 10 data sets as they will have no style and |
||||||
be in black. |
be in black. |
||||||
|
|
||||||
Note that multiple data sets within the same chart can differ in |
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; |
length, and that the data in the datasets needn't be in order; |
||||||
they will be ordered by the plot along the X-axis. |
they will be ordered by the plot along the X-axis. |
||||||
|
|
||||||
The dates must be parseable by ParseDate, but otherwise can be |
The dates must be parseable by ParseDate, but otherwise can be |
||||||
any order of magnitude (seconds within the hour, or years) |
any order of magnitude (seconds within the hour, or years) |
||||||
|
|
||||||
= See also |
= See also |
||||||
|
|
||||||
* SVG::Graph::Graph |
* SVG::Graph::Graph |
||||||
* SVG::Graph::BarHorizontal |
* SVG::Graph::BarHorizontal |
||||||
* SVG::Graph::Bar |
* SVG::Graph::Bar |
||||||
* SVG::Graph::Line |
* SVG::Graph::Line |
||||||
* SVG::Graph::Pie |
* SVG::Graph::Pie |
||||||
* SVG::Graph::Plot |
* SVG::Graph::Plot |
||||||
* SVG::Graph::TimeSeries |
* SVG::Graph::TimeSeries |
||||||
|
|
||||||
== Author |
== Author |
||||||
|
|
||||||
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> |
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> |
||||||
|
|
||||||
Copyright 2004 Sean E. Russell |
Copyright 2004 Sean E. Russell |
||||||
This software is available under the Ruby license[LICENSE.txt] |
This software is available under the Ruby license[LICENSE.txt] |
||||||
|
|
||||||
""" |
""" |
||||||
|
|
||||||
"The format string to be used to format the X axis labels" |
"The format string to be used to format the X axis labels" |
||||||
x_label_format = '%Y-%m-%d %H:%M:%S' |
x_label_format = '%Y-%m-%d %H:%M:%S' |
||||||
|
|
||||||
""" |
""" |
||||||
Use this to set the spacing between dates on the axis. The value |
Use this to set the spacing between dates on the axis. The value |
||||||
must be of the form |
must be of the form |
||||||
"\d+ ?((year|month|week|day|hour|minute|second)s?)?" |
"\d+ ?((year|month|week|day|hour|minute|second)s?)?" |
||||||
|
|
||||||
e.g. |
e.g. |
||||||
|
|
||||||
graph.timescale_divisions = '2 weeks' |
graph.timescale_divisions = '2 weeks' |
||||||
graph.timescale_divisions = '1 month' |
graph.timescale_divisions = '1 month' |
||||||
graph.timescale_divisions = '3600 seconds' # easier would be '1 hour' |
graph.timescale_divisions = '3600 seconds' # easier would be '1 hour' |
||||||
""" |
""" |
||||||
timescale_divisions = None |
timescale_divisions = None |
||||||
|
|
||||||
"The formatting used for the popups. See x_label_format" |
"The formatting used for the popups. See x_label_format" |
||||||
popup_format = '%Y-%m-%d %H:%M:%S' |
popup_format = '%Y-%m-%d %H:%M:%S' |
||||||
|
|
||||||
_min_x_value = None |
_min_x_value = None |
||||||
scale_x_divisions = False |
scale_x_divisions = False |
||||||
scale_x_integers = False |
scale_x_integers = False |
||||||
bar_gap = True |
bar_gap = True |
||||||
|
|
||||||
stylesheet_names = Graph.stylesheet_names + ['bar.css'] |
stylesheet_names = Graph.stylesheet_names + ['bar.css'] |
||||||
|
|
||||||
def add_data(self, data): |
def add_data(self, data): |
||||||
""" |
""" |
||||||
Add data to the plot. |
Add data to the plot. |
||||||
|
|
||||||
# A data set with 1 point: Lunch from 12:30 to 14:00 |
# A data set with 1 point: Lunch from 12:30 to 14:00 |
||||||
d1 = [ "Lunch", "12:30", "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 |
# 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 |
# "Henry V" runs from 6/12/03 to 8/20/03 |
||||||
d2 = [ "Cats", "5/11/03", "7/15/04", |
d2 = [ "Cats", "5/11/03", "7/15/04", |
||||||
"Henry V", "6/12/03", "8/20/03" ] |
"Henry V", "6/12/03", "8/20/03" ] |
||||||
|
|
||||||
graph.add_data( |
graph.add_data( |
||||||
:data => d1, |
:data => d1, |
||||||
:title => 'Meetings' |
:title => 'Meetings' |
||||||
) |
) |
||||||
graph.add_data( |
graph.add_data( |
||||||
:data => d2, |
:data => d2, |
||||||
:title => 'Plays' |
:title => 'Plays' |
||||||
) |
) |
||||||
|
|
||||||
Note that the data must be in time,value pairs, |
Note that the data must be in time,value pairs, |
||||||
and that the date format |
and that the date format |
||||||
may be any date that is parseable by ParseDate. |
may be any date that is parseable by ParseDate. |
||||||
Also note that, in this example, we're mixing scales; the data from d1 |
Also note that, in this example, we're mixing scales; the data from d1 |
||||||
will probably not be discernable if both data sets are |
will probably not be discernable if both data sets are |
||||||
plotted on the same graph, since d1 is too granular. |
plotted on the same graph, since d1 is too granular. |
||||||
""" |
""" |
||||||
# The ruby version does something different here, throwing out |
# The ruby version does something different here, throwing out |
||||||
# any previously added data. |
# any previously added data. |
||||||
super(Schedule, self).add_data(data) |
super(Schedule, self).add_data(data) |
||||||
|
|
||||||
# copied from Bar |
# copied from Bar |
||||||
# TODO, refactor this into a common base class (or mix-in) |
# TODO, refactor this into a common base class (or mix-in) |
||||||
def get_bar_gap(self, field_size): |
def get_bar_gap(self, field_size): |
||||||
bar_gap = 10 # default gap |
bar_gap = 10 # default gap |
||||||
if field_size < 10: |
if field_size < 10: |
||||||
# adjust for narrow fields |
# adjust for narrow fields |
||||||
bar_gap = field_size / 2 |
bar_gap = field_size / 2 |
||||||
# the following zero's out the gap if bar_gap is False |
# the following zero's out the gap if bar_gap is False |
||||||
bar_gap = int(self.bar_gap) * bar_gap |
bar_gap = int(self.bar_gap) * bar_gap |
||||||
return bar_gap |
return bar_gap |
||||||
|
|
||||||
def validate_data(self, conf): |
def validate_data(self, conf): |
||||||
super(Schedule, self).validate_data(conf) |
super(Schedule, self).validate_data(conf) |
||||||
msg = "Data supplied must be (title, from, to) tripples!" |
msg = "Data supplied must be (title, from, to) tripples!" |
||||||
assert len(conf['data']) % 3 == 0, msg |
assert len(conf['data']) % 3 == 0, msg |
||||||
|
|
||||||
def process_data(self, conf): |
def process_data(self, conf): |
||||||
super(Schedule, self).process_data(conf) |
super(Schedule, self).process_data(conf) |
||||||
data = conf['data'] |
data = conf['data'] |
||||||
triples = grouper(3, data) |
triples = grouper(3, data) |
||||||
|
|
||||||
labels, begin_dates, end_dates = zip(*triples) |
labels, begin_dates, end_dates = zip(*triples) |
||||||
|
|
||||||
begin_dates = map(self.parse_date, begin_dates) |
begin_dates = map(self.parse_date, begin_dates) |
||||||
end_dates = map(self.parse_date, end_dates) |
end_dates = map(self.parse_date, end_dates) |
||||||
|
|
||||||
# reconstruct the triples in a new order |
# reconstruct the triples in a new order |
||||||
reordered_triples = zip(begin_dates, end_dates, labels) |
reordered_triples = zip(begin_dates, end_dates, labels) |
||||||
|
|
||||||
# because of the reordering, this will sort by begin_date |
# because of the reordering, this will sort by begin_date |
||||||
# then end_date, then label. |
# then end_date, then label. |
||||||
reordered_triples.sort() |
reordered_triples.sort() |
||||||
|
|
||||||
conf['data'] = reordered_triples |
conf['data'] = reordered_triples |
||||||
|
|
||||||
def parse_date(self, date_string): |
def parse_date(self, date_string): |
||||||
return parse(date_string) |
return parse(date_string) |
||||||
|
|
||||||
def set_min_x_value(self, value): |
def set_min_x_value(self, value): |
||||||
if isinstance(value, basestring): |
if isinstance(value, basestring): |
||||||
value = self.parse_date(value) |
value = self.parse_date(value) |
||||||
self._min_x_value = value |
self._min_x_value = value |
||||||
|
|
||||||
def get_min_x_value(self): |
def get_min_x_value(self): |
||||||
return self._min_x_value |
return self._min_x_value |
||||||
|
|
||||||
min_x_value = property(get_min_x_value, set_min_x_value) |
min_x_value = property(get_min_x_value, set_min_x_value) |
||||||
|
|
||||||
def format(self, x, y): |
def format(self, x, y): |
||||||
return x.strftime(self.popup_format) |
return x.strftime(self.popup_format) |
||||||
|
|
||||||
def get_x_labels(self): |
def get_x_labels(self): |
||||||
format = lambda x: x.strftime(self.x_label_format) |
format = lambda x: x.strftime(self.x_label_format) |
||||||
return map(format, self.get_x_values()) |
return map(format, self.get_x_values()) |
||||||
|
|
||||||
def y_label_offset(self, height): |
def y_label_offset(self, height): |
||||||
return height / -2.0 |
return height / -2.0 |
||||||
|
|
||||||
def get_y_labels(self): |
def get_y_labels(self): |
||||||
# ruby version uses the last data supplied |
# ruby version uses the last data supplied |
||||||
last = -1 |
last = -1 |
||||||
data = self.data[last]['data'] |
data = self.data[last]['data'] |
||||||
begin_dates, start_dates, labels = zip(*data) |
begin_dates, start_dates, labels = zip(*data) |
||||||
return labels |
return labels |
||||||
|
|
||||||
def draw_data(self): |
def draw_data(self): |
||||||
bar_gap = self.get_bar_gap(self.get_field_height()) |
bar_gap = self.get_bar_gap(self.get_field_height()) |
||||||
|
|
||||||
subbar_height = self.get_field_height() - bar_gap |
subbar_height = self.get_field_height() - bar_gap |
||||||
|
|
||||||
y_mod = (subbar_height / 2) + (self.font_size / 2) |
y_mod = (subbar_height / 2) + (self.font_size / 2) |
||||||
x_min, x_max, div = self._x_range() |
x_min, x_max, div = self._x_range() |
||||||
x_range = x_max - x_min |
x_range = x_max - x_min |
||||||
width = (float(self.graph_width) - self.font_size * 2) |
width = (float(self.graph_width) - self.font_size * 2) |
||||||
# time_scale |
# time_scale |
||||||
#scale /= x_range |
#scale /= x_range |
||||||
scale = TimeScale(width, x_range) |
scale = TimeScale(width, x_range) |
||||||
|
|
||||||
# ruby version uses the last data supplied |
# ruby version uses the last data supplied |
||||||
last = -1 |
last = -1 |
||||||
data = self.data[last]['data'] |
data = self.data[last]['data'] |
||||||
|
|
||||||
for index, (x_start, x_end, label) in enumerate(data): |
for index, (x_start, x_end, label) in enumerate(data): |
||||||
count = index + 1 # index is 0-based, count is 1-based |
count = index + 1 # index is 0-based, count is 1-based |
||||||
y = self.graph_height - (self.get_field_height() * count) |
y = self.graph_height - (self.get_field_height() * count) |
||||||
bar_width = scale * (x_end - x_start) |
bar_width = scale * (x_end - x_start) |
||||||
bar_start = scale * (x_start - x_min) |
bar_start = scale * (x_start - x_min) |
||||||
|
|
||||||
etree.SubElement(self.graph, 'rect', { |
etree.SubElement(self.graph, 'rect', { |
||||||
'x': str(bar_start), |
'x': str(bar_start), |
||||||
'y': str(y), |
'y': str(y), |
||||||
'width': str(bar_width), |
'width': str(bar_width), |
||||||
'height': str(subbar_height), |
'height': str(subbar_height), |
||||||
'class': 'fill%s' % (count + 1), |
'class': 'fill%s' % (count + 1), |
||||||
}) |
}) |
||||||
|
|
||||||
def _x_range(self): |
def _x_range(self): |
||||||
# ruby version uses teh last data supplied |
# ruby version uses teh last data supplied |
||||||
last = -1 |
last = -1 |
||||||
data = self.data[last]['data'] |
data = self.data[last]['data'] |
||||||
|
|
||||||
start_dates, end_dates, labels = zip(*data) |
start_dates, end_dates, labels = zip(*data) |
||||||
all_dates = start_dates + end_dates |
all_dates = start_dates + end_dates |
||||||
max_value = max(all_dates) |
max_value = max(all_dates) |
||||||
if not self.min_x_value is None: |
if not self.min_x_value is None: |
||||||
all_dates.append(self.min_x_value) |
all_dates.append(self.min_x_value) |
||||||
min_value = min(all_dates) |
min_value = min(all_dates) |
||||||
range = max_value - min_value |
range = max_value - min_value |
||||||
right_pad = divide_timedelta_float( |
right_pad = divide_timedelta_float( |
||||||
range, 20.0) or relativedelta(days=10) |
range, 20.0) or relativedelta(days=10) |
||||||
scale_range = (max_value + right_pad) - min_value |
scale_range = (max_value + right_pad) - min_value |
||||||
|
|
||||||
#scale_division = self.scale_x_divisions or (scale_range / 10.0) |
#scale_division = self.scale_x_divisions or (scale_range / 10.0) |
||||||
# todo, remove timescale_x_divisions and use scale_x_divisions only |
# todo, remove timescale_x_divisions and use scale_x_divisions only |
||||||
# but as a time delta |
# but as a time delta |
||||||
scale_division = divide_timedelta_float(scale_range, 10.0) |
scale_division = divide_timedelta_float(scale_range, 10.0) |
||||||
|
|
||||||
# this doesn't make sense, because x is a timescale |
# this doesn't make sense, because x is a timescale |
||||||
#if self.scale_x_integers: |
#if self.scale_x_integers: |
||||||
# scale_division = min(round(scale_division), 1) |
# scale_division = min(round(scale_division), 1) |
||||||
|
|
||||||
return min_value, max_value, scale_division |
return min_value, max_value, scale_division |
||||||
|
|
||||||
def get_x_values(self): |
def get_x_values(self): |
||||||
x_min, x_max, scale_division = self._x_range() |
x_min, x_max, scale_division = self._x_range() |
||||||
if self.timescale_divisions: |
if self.timescale_divisions: |
||||||
pattern = re.compile('(\d+) ?(\w+)') |
pattern = re.compile('(\d+) ?(\w+)') |
||||||
m = pattern.match(self.timescale_divisions) |
m = pattern.match(self.timescale_divisions) |
||||||
if not m: |
if not m: |
||||||
raise (ValueError, |
raise (ValueError, |
||||||
"Invalid timescale_divisions: %s" % |
"Invalid timescale_divisions: %s" % |
||||||
self.timescale_divisions) |
self.timescale_divisions) |
||||||
|
|
||||||
magnitude = int(m.group(1)) |
magnitude = int(m.group(1)) |
||||||
units = m.group(2) |
units = m.group(2) |
||||||
|
|
||||||
parameter = self.lookup_relativedelta_parameter(units) |
parameter = self.lookup_relativedelta_parameter(units) |
||||||
|
|
||||||
delta = relativedelta(**{parameter: magnitude}) |
delta = relativedelta(**{parameter: magnitude}) |
||||||
|
|
||||||
scale_division = delta |
scale_division = delta |
||||||
|
|
||||||
return date_range(x_min, x_max, scale_division) |
return date_range(x_min, x_max, scale_division) |
||||||
|
|
||||||
def lookup_relativedelta_parameter(self, unit_string): |
def lookup_relativedelta_parameter(self, unit_string): |
||||||
from util import reverse_mapping, flatten_mapping |
from util import reverse_mapping, flatten_mapping |
||||||
unit_string = unit_string.lower() |
unit_string = unit_string.lower() |
||||||
mapping = dict( |
mapping = dict( |
||||||
years=('years', 'year', 'yrs', 'yr'), |
years=('years', 'year', 'yrs', 'yr'), |
||||||
months=('months', 'month', 'mo'), |
months=('months', 'month', 'mo'), |
||||||
weeks=('weeks', 'week', 'wks', 'wk'), |
weeks=('weeks', 'week', 'wks', 'wk'), |
||||||
days=('days', 'day'), |
days=('days', 'day'), |
||||||
hours=('hours', 'hour', 'hr', 'hrs', 'h'), |
hours=('hours', 'hour', 'hr', 'hrs', 'h'), |
||||||
minutes=('minutes', 'minute', 'min', 'mins', 'm'), |
minutes=('minutes', 'minute', 'min', 'mins', 'm'), |
||||||
seconds=('seconds', 'second', 'sec', 'secs', 's'), |
seconds=('seconds', 'second', 'sec', 'secs', 's'), |
||||||
) |
) |
||||||
mapping = reverse_mapping(mapping) |
mapping = reverse_mapping(mapping) |
||||||
mapping = flatten_mapping(mapping) |
mapping = flatten_mapping(mapping) |
||||||
if not unit_string in mapping: |
if not unit_string in mapping: |
||||||
raise ValueError("%s doesn't match any supported time/date unit") |
raise ValueError("%s doesn't match any supported time/date unit") |
||||||
return mapping[unit_string] |
return mapping[unit_string] |
||||||
|
@ -1,199 +1,199 @@ |
|||||||
#!/usr/bin/env python |
#!/usr/bin/env python |
||||||
import pygal.plot |
import pygal.plot |
||||||
import re |
import re |
||||||
import pkg_resources |
import pkg_resources |
||||||
pkg_resources.require("python-dateutil>=1.1") |
pkg_resources.require("python-dateutil>=1.1") |
||||||
from dateutil.parser import parse |
from dateutil.parser import parse |
||||||
from dateutil.relativedelta import relativedelta |
from dateutil.relativedelta import relativedelta |
||||||
from time import mktime |
from time import mktime |
||||||
import datetime |
import datetime |
||||||
fromtimestamp = datetime.datetime.fromtimestamp |
fromtimestamp = datetime.datetime.fromtimestamp |
||||||
from .util import float_range |
from .util import float_range |
||||||
|
|
||||||
|
|
||||||
class Plot(pygal.plot.Plot): |
class Plot(pygal.plot.Plot): |
||||||
"""=== For creating SVG plots of scalar temporal data |
"""=== For creating SVG plots of scalar temporal data |
||||||
|
|
||||||
= Synopsis |
= Synopsis |
||||||
|
|
||||||
import SVG.TimeSeries |
import SVG.TimeSeries |
||||||
|
|
||||||
# Data sets are x,y pairs |
# Data sets are x,y pairs |
||||||
data1 = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11, |
data1 = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11, |
||||||
"9/11/01", 9, "9/1/85", 2, |
"9/11/01", 9, "9/1/85", 2, |
||||||
"9/1/88", 1, "1/15/95", 13] |
"9/1/88", 1, "1/15/95", 13] |
||||||
data2 = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4, |
data2 = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4, |
||||||
"5/1/02", 14, "3/1/95", 6, |
"5/1/02", 14, "3/1/95", 6, |
||||||
"8/1/91", 12, "12/1/87", 6, |
"8/1/91", 12, "12/1/87", 6, |
||||||
"5/1/84", 17, "10/1/80", 12] |
"5/1/84", 17, "10/1/80", 12] |
||||||
|
|
||||||
graph = SVG::Graph::TimeSeries.new({ |
graph = SVG::Graph::TimeSeries.new({ |
||||||
:width => 640, |
:width => 640, |
||||||
:height => 480, |
:height => 480, |
||||||
:graph_title => title, |
:graph_title => title, |
||||||
:show_graph_title => true, |
:show_graph_title => true, |
||||||
:no_css => true, |
:no_css => true, |
||||||
:key => true, |
:key => true, |
||||||
:scale_x_integers => true, |
:scale_x_integers => true, |
||||||
:scale_y_integers => true, |
:scale_y_integers => true, |
||||||
:min_x_value => 0, |
:min_x_value => 0, |
||||||
:min_y_value => 0, |
:min_y_value => 0, |
||||||
:show_data_labels => true, |
:show_data_labels => true, |
||||||
:show_x_guidelines => true, |
:show_x_guidelines => true, |
||||||
:show_x_title => true, |
:show_x_title => true, |
||||||
:x_title => "Time", |
:x_title => "Time", |
||||||
:show_y_title => true, |
:show_y_title => true, |
||||||
:y_title => "Ice Cream Cones", |
:y_title => "Ice Cream Cones", |
||||||
:y_title_text_direction => :bt, |
:y_title_text_direction => :bt, |
||||||
:stagger_x_labels => true, |
:stagger_x_labels => true, |
||||||
:x_label_format => "%m/%d/%y", |
:x_label_format => "%m/%d/%y", |
||||||
}) |
}) |
||||||
|
|
||||||
graph.add_data({ |
graph.add_data({ |
||||||
:data => projection |
:data => projection |
||||||
:title => 'Projected', |
:title => 'Projected', |
||||||
}) |
}) |
||||||
|
|
||||||
graph.add_data({ |
graph.add_data({ |
||||||
:data => actual, |
:data => actual, |
||||||
:title => 'Actual', |
:title => 'Actual', |
||||||
}) |
}) |
||||||
|
|
||||||
print graph.burn() |
print graph.burn() |
||||||
|
|
||||||
= Description |
= Description |
||||||
|
|
||||||
Produces a graph of temporal scalar data. |
Produces a graph of temporal scalar data. |
||||||
|
|
||||||
= Examples |
= Examples |
||||||
|
|
||||||
http://www.germane-software/repositories/public/SVG/test/timeseries.rb |
http://www.germane-software/repositories/public/SVG/test/timeseries.rb |
||||||
|
|
||||||
= Notes |
= Notes |
||||||
|
|
||||||
The default stylesheet handles upto 10 data sets, if you |
The default stylesheet handles upto 10 data sets, if you |
||||||
use more you must create your own stylesheet and add the |
use more you must create your own stylesheet and add the |
||||||
additional settings for the extra data sets. You will know |
additional settings for the extra data sets. You will know |
||||||
if you go over 10 data sets as they will have no style and |
if you go over 10 data sets as they will have no style and |
||||||
be in black. |
be in black. |
||||||
|
|
||||||
Unlike the other types of charts, data sets must contain x,y pairs: |
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) |
["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 |
["01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and |
||||||
# ("14:20",6) |
# ("14:20",6) |
||||||
|
|
||||||
Note that multiple data sets within |
Note that multiple data sets within |
||||||
the same chart can differ in length, |
the same chart can differ in length, |
||||||
and that the data in the datasets needn't be in order; |
and that the data in the datasets needn't be in order; |
||||||
they will be ordered by the plot along the X-axis. |
they will be ordered by the plot along the X-axis. |
||||||
|
|
||||||
The dates must be parseable by ParseDate, but otherwise can be |
The dates must be parseable by ParseDate, but otherwise can be |
||||||
any order of magnitude (seconds within the hour, or years) |
any order of magnitude (seconds within the hour, or years) |
||||||
|
|
||||||
= See also |
= See also |
||||||
|
|
||||||
* SVG::Graph::Graph |
* SVG::Graph::Graph |
||||||
* SVG::Graph::BarHorizontal |
* SVG::Graph::BarHorizontal |
||||||
* SVG::Graph::Bar |
* SVG::Graph::Bar |
||||||
* SVG::Graph::Line |
* SVG::Graph::Line |
||||||
* SVG::Graph::Pie |
* SVG::Graph::Pie |
||||||
* SVG::Graph::Plot |
* SVG::Graph::Plot |
||||||
|
|
||||||
== Author |
== Author |
||||||
|
|
||||||
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> |
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> |
||||||
|
|
||||||
Copyright 2004 Sean E. Russell |
Copyright 2004 Sean E. Russell |
||||||
This software is available under the Ruby license[LICENSE.txt] |
This software is available under the Ruby license[LICENSE.txt] |
||||||
""" |
""" |
||||||
popup_format = x_label_format = '%Y-%m-%d %H:%M:%S' |
popup_format = x_label_format = '%Y-%m-%d %H:%M:%S' |
||||||
__doc_popup_format_ = ("The formatting usped for the popups." |
__doc_popup_format_ = ("The formatting usped for the popups." |
||||||
" See x_label_format") |
" See x_label_format") |
||||||
__doc_x_label_format_ = ("The format string used to format " |
__doc_x_label_format_ = ("The format string used to format " |
||||||
"the X axis labels. See strftime.") |
"the X axis labels. See strftime.") |
||||||
|
|
||||||
timescale_divisions = None |
timescale_divisions = None |
||||||
__doc_timescale_divisions_ = """Use this to set the spacing |
__doc_timescale_divisions_ = """Use this to set the spacing |
||||||
between dates on the axis. The value must be of the form |
between dates on the axis. The value must be of the form |
||||||
"\d+ ?(days|weeks|months|years|hours|minutes|seconds)?" |
"\d+ ?(days|weeks|months|years|hours|minutes|seconds)?" |
||||||
|
|
||||||
EG: |
EG: |
||||||
|
|
||||||
graph.timescale_divisions = "2 weeks" |
graph.timescale_divisions = "2 weeks" |
||||||
|
|
||||||
will cause the chart to try to divide the X axis up into segments of |
will cause the chart to try to divide the X axis up into segments of |
||||||
two week periods.""" |
two week periods.""" |
||||||
|
|
||||||
def add_data(self, data): |
def add_data(self, data): |
||||||
"""Add data to the plot. |
"""Add data to the plot. |
||||||
d1 = ["12:30", 2] # A data set with 1 point: ("12:30",2) |
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: |
d2 = ["01:00",2, "14:20",6] # A data set with 2 points: |
||||||
# ("01:00",2) and ("14:20",6) |
# ("01:00",2) and ("14:20",6) |
||||||
graph.add_data( |
graph.add_data( |
||||||
:data => d1, |
:data => d1, |
||||||
:title => 'One' |
:title => 'One' |
||||||
) |
) |
||||||
graph.add_data( |
graph.add_data( |
||||||
:data => d2, |
:data => d2, |
||||||
:title => 'Two' |
:title => 'Two' |
||||||
) |
) |
||||||
|
|
||||||
Note that the data must be in time,value pairs, |
Note that the data must be in time,value pairs, |
||||||
and that the date format |
and that the date format |
||||||
may be any date that is parseable by ParseDate.""" |
may be any date that is parseable by ParseDate.""" |
||||||
super(Plot, self).add_data(data) |
super(Plot, self).add_data(data) |
||||||
|
|
||||||
def process_data(self, data): |
def process_data(self, data): |
||||||
super(Plot, self).process_data(data) |
super(Plot, self).process_data(data) |
||||||
# the date should be in the first element, so parse it out |
# the date should be in the first element, so parse it out |
||||||
data['data'][0] = map(self.parse_date, data['data'][0]) |
data['data'][0] = map(self.parse_date, data['data'][0]) |
||||||
|
|
||||||
_min_x_value = pygal.plot.Plot.min_x_value |
_min_x_value = pygal.plot.Plot.min_x_value |
||||||
|
|
||||||
def get_min_x_value(self): |
def get_min_x_value(self): |
||||||
return self._min_x_value |
return self._min_x_value |
||||||
|
|
||||||
def set_min_x_value(self, date): |
def set_min_x_value(self, date): |
||||||
self._min_x_value = self.parse_date(date) |
self._min_x_value = self.parse_date(date) |
||||||
min_x_value = property(get_min_x_value, set_min_x_value) |
min_x_value = property(get_min_x_value, set_min_x_value) |
||||||
|
|
||||||
def format(self, x, y): |
def format(self, x, y): |
||||||
return fromtimestamp(x).strftime(self.popup_format) |
return fromtimestamp(x).strftime(self.popup_format) |
||||||
|
|
||||||
def get_x_labels(self): |
def get_x_labels(self): |
||||||
return map(lambda t: fromtimestamp(t).strftime(self.x_label_format), |
return map(lambda t: fromtimestamp(t).strftime(self.x_label_format), |
||||||
self.get_x_values()) |
self.get_x_values()) |
||||||
|
|
||||||
def get_x_values(self): |
def get_x_values(self): |
||||||
result = self.get_x_timescale_division_values() |
result = self.get_x_timescale_division_values() |
||||||
if result: |
if result: |
||||||
return result |
return result |
||||||
return tuple(float_range(*self.x_range())) |
return tuple(float_range(*self.x_range())) |
||||||
|
|
||||||
def get_x_timescale_division_values(self): |
def get_x_timescale_division_values(self): |
||||||
if not self.timescale_divisions: |
if not self.timescale_divisions: |
||||||
return |
return |
||||||
min, max, scale_division = self.x_range() |
min, max, scale_division = self.x_range() |
||||||
m = re.match( |
m = re.match( |
||||||
'(?P<amount>\d+) ?(?P<division_units>' |
'(?P<amount>\d+) ?(?P<division_units>' |
||||||
'days|weeks|months|years|hours|minutes|seconds)?', |
'days|weeks|months|years|hours|minutes|seconds)?', |
||||||
self.timescale_divisions) |
self.timescale_divisions) |
||||||
# copy amount and division_units into the local namespace |
# copy amount and division_units into the local namespace |
||||||
division_units = m.groupdict()['division_units'] or 'days' |
division_units = m.groupdict()['division_units'] or 'days' |
||||||
amount = int(m.groupdict()['amount']) |
amount = int(m.groupdict()['amount']) |
||||||
if not amount: |
if not amount: |
||||||
return |
return |
||||||
delta = relativedelta(**{division_units: amount}) |
delta = relativedelta(**{division_units: amount}) |
||||||
result = tuple(self.get_time_range(min, max, delta)) |
result = tuple(self.get_time_range(min, max, delta)) |
||||||
return result |
return result |
||||||
|
|
||||||
def get_time_range(self, start, stop, delta): |
def get_time_range(self, start, stop, delta): |
||||||
start, stop = map(fromtimestamp, (start, stop)) |
start, stop = map(fromtimestamp, (start, stop)) |
||||||
current = start |
current = start |
||||||
while current <= stop: |
while current <= stop: |
||||||
yield mktime(current.timetuple()) |
yield mktime(current.timetuple()) |
||||||
current += delta |
current += delta |
||||||
|
|
||||||
def parse_date(self, date_string): |
def parse_date(self, date_string): |
||||||
return mktime(parse(date_string).timetuple()) |
return mktime(parse(date_string).timetuple()) |
||||||
|
@ -1,178 +1,178 @@ |
|||||||
#!python |
#!python |
||||||
|
|
||||||
import itertools |
import itertools |
||||||
import datetime |
import datetime |
||||||
# from itertools recipes (python documentation) |
# from itertools recipes (python documentation) |
||||||
|
|
||||||
|
|
||||||
def grouper(n, iterable, padvalue=None): |
def grouper(n, iterable, padvalue=None): |
||||||
""" |
""" |
||||||
>>> tuple(grouper(3, 'abcdefg', 'x')) |
>>> tuple(grouper(3, 'abcdefg', 'x')) |
||||||
(('a', 'b', 'c'), ('d', 'e', 'f'), ('g', 'x', 'x')) |
(('a', 'b', 'c'), ('d', 'e', 'f'), ('g', 'x', 'x')) |
||||||
""" |
""" |
||||||
return itertools.izip( |
return itertools.izip( |
||||||
*[itertools.chain(iterable, |
*[itertools.chain(iterable, |
||||||
itertools.repeat(padvalue, n - 1))] * n) |
itertools.repeat(padvalue, n - 1))] * n) |
||||||
|
|
||||||
|
|
||||||
def reverse_mapping(mapping): |
def reverse_mapping(mapping): |
||||||
""" |
""" |
||||||
For every key, value pair, return the mapping for the |
For every key, value pair, return the mapping for the |
||||||
equivalent value, key pair |
equivalent value, key pair |
||||||
>>> reverse_mapping({'a': 'b'}) == {'b': 'a'} |
>>> reverse_mapping({'a': 'b'}) == {'b': 'a'} |
||||||
True |
True |
||||||
""" |
""" |
||||||
keys, values = zip(*mapping.items()) |
keys, values = zip(*mapping.items()) |
||||||
return dict(zip(values, keys)) |
return dict(zip(values, keys)) |
||||||
|
|
||||||
|
|
||||||
def flatten_mapping(mapping): |
def flatten_mapping(mapping): |
||||||
""" |
""" |
||||||
For every key that has an __iter__ method, assign the values |
For every key that has an __iter__ method, assign the values |
||||||
to a key for each. |
to a key for each. |
||||||
>>> flatten_mapping({'ab': 3, ('c','d'): 4}) == {'ab': 3, 'c': 4, 'd': 4} |
>>> flatten_mapping({'ab': 3, ('c','d'): 4}) == {'ab': 3, 'c': 4, 'd': 4} |
||||||
True |
True |
||||||
""" |
""" |
||||||
return dict(flatten_items(mapping.items())) |
return dict(flatten_items(mapping.items())) |
||||||
|
|
||||||
|
|
||||||
def flatten_items(items): |
def flatten_items(items): |
||||||
for keys, value in items: |
for keys, value in items: |
||||||
if hasattr(keys, '__iter__'): |
if hasattr(keys, '__iter__'): |
||||||
for key in keys: |
for key in keys: |
||||||
yield (key, value) |
yield (key, value) |
||||||
else: |
else: |
||||||
yield (keys, value) |
yield (keys, value) |
||||||
|
|
||||||
|
|
||||||
def float_range(start=0, stop=None, step=1): |
def float_range(start=0, stop=None, step=1): |
||||||
""" |
""" |
||||||
Much like the built-in function range, but accepts floats |
Much like the built-in function range, but accepts floats |
||||||
>>> tuple(float_range(0, 9, 1.5)) |
>>> tuple(float_range(0, 9, 1.5)) |
||||||
(0.0, 1.5, 3.0, 4.5, 6.0, 7.5) |
(0.0, 1.5, 3.0, 4.5, 6.0, 7.5) |
||||||
""" |
""" |
||||||
start = float(start) |
start = float(start) |
||||||
while start < stop: |
while start < stop: |
||||||
yield start |
yield start |
||||||
start += step |
start += step |
||||||
|
|
||||||
|
|
||||||
def date_range(start=None, stop=None, step=None): |
def date_range(start=None, stop=None, step=None): |
||||||
""" |
""" |
||||||
Much like the built-in function range, but works with dates |
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))) |
>>> my_range = tuple(date_range(datetime.datetime(2005,12,21), datetime.datetime(2005,12,25))) |
||||||
>>> datetime.datetime(2005,12,21) in my_range |
>>> datetime.datetime(2005,12,21) in my_range |
||||||
True |
True |
||||||
>>> datetime.datetime(2005,12,22) in my_range |
>>> datetime.datetime(2005,12,22) in my_range |
||||||
True |
True |
||||||
>>> datetime.datetime(2005,12,25) in my_range |
>>> datetime.datetime(2005,12,25) in my_range |
||||||
False |
False |
||||||
""" |
""" |
||||||
if step is None: |
if step is None: |
||||||
step = datetime.timedelta(days=1) |
step = datetime.timedelta(days=1) |
||||||
if start is None: |
if start is None: |
||||||
start = datetime.datetime.now() |
start = datetime.datetime.now() |
||||||
while start < stop: |
while start < stop: |
||||||
yield start |
yield start |
||||||
start += step |
start += step |
||||||
|
|
||||||
|
|
||||||
# copied from jaraco.datetools |
# copied from jaraco.datetools |
||||||
def divide_timedelta_float(td, divisor): |
def divide_timedelta_float(td, divisor): |
||||||
""" |
""" |
||||||
Meant to work around the limitation that Python datetime doesn't support |
Meant to work around the limitation that Python datetime doesn't support |
||||||
floats as divisors or multiplicands to datetime objects |
floats as divisors or multiplicands to datetime objects |
||||||
>>> one_day = datetime.timedelta(days=1) |
>>> one_day = datetime.timedelta(days=1) |
||||||
>>> half_day = datetime.timedelta(days=.5) |
>>> half_day = datetime.timedelta(days=.5) |
||||||
>>> divide_timedelta_float(one_day, 2.0) == half_day |
>>> divide_timedelta_float(one_day, 2.0) == half_day |
||||||
True |
True |
||||||
>>> divide_timedelta_float(one_day, 2) == half_day |
>>> divide_timedelta_float(one_day, 2) == half_day |
||||||
False |
False |
||||||
""" |
""" |
||||||
# td is comprised of days, seconds, microseconds |
# td is comprised of days, seconds, microseconds |
||||||
dsm = [getattr(td, attr) for attr in ('days', 'seconds', 'microseconds')] |
dsm = [getattr(td, attr) for attr in ('days', 'seconds', 'microseconds')] |
||||||
dsm = map(lambda elem: elem / divisor, dsm) |
dsm = map(lambda elem: elem / divisor, dsm) |
||||||
return datetime.timedelta(*dsm) |
return datetime.timedelta(*dsm) |
||||||
|
|
||||||
|
|
||||||
def get_timedelta_total_microseconds(td): |
def get_timedelta_total_microseconds(td): |
||||||
seconds = td.days * 86400 + td.seconds |
seconds = td.days * 86400 + td.seconds |
||||||
microseconds = td.microseconds + seconds * (10 ** 6) |
microseconds = td.microseconds + seconds * (10 ** 6) |
||||||
return microseconds |
return microseconds |
||||||
|
|
||||||
|
|
||||||
def divide_timedelta(td1, td2): |
def divide_timedelta(td1, td2): |
||||||
""" |
""" |
||||||
Get the ratio of two timedeltas |
Get the ratio of two timedeltas |
||||||
>>> one_day = datetime.timedelta(days=1) |
>>> one_day = datetime.timedelta(days=1) |
||||||
>>> one_hour = datetime.timedelta(hours=1) |
>>> one_hour = datetime.timedelta(hours=1) |
||||||
>>> divide_timedelta(one_hour, one_day) == 1/24.0 |
>>> divide_timedelta(one_hour, one_day) == 1/24.0 |
||||||
True |
True |
||||||
""" |
""" |
||||||
|
|
||||||
td1_total = float(get_timedelta_total_microseconds(td1)) |
td1_total = float(get_timedelta_total_microseconds(td1)) |
||||||
td2_total = float(get_timedelta_total_microseconds(td2)) |
td2_total = float(get_timedelta_total_microseconds(td2)) |
||||||
return td1_total / td2_total |
return td1_total / td2_total |
||||||
|
|
||||||
|
|
||||||
class TimeScale(object): |
class TimeScale(object): |
||||||
"Describes a scale factor based on time instead of a scalar" |
"Describes a scale factor based on time instead of a scalar" |
||||||
def __init__(self, width, range): |
def __init__(self, width, range): |
||||||
self.width = width |
self.width = width |
||||||
self.range = range |
self.range = range |
||||||
|
|
||||||
def __mul__(self, delta): |
def __mul__(self, delta): |
||||||
scale = divide_timedelta(delta, self.range) |
scale = divide_timedelta(delta, self.range) |
||||||
return scale * self.width |
return scale * self.width |
||||||
|
|
||||||
|
|
||||||
# the following three functions were copied from jaraco.util.iter_ |
# the following three functions were copied from jaraco.util.iter_ |
||||||
# todo, factor out caching capability |
# todo, factor out caching capability |
||||||
class iterable_test(dict): |
class iterable_test(dict): |
||||||
"Test objects for iterability, caching the result by type" |
"Test objects for iterability, caching the result by type" |
||||||
def __init__(self, ignore_classes=(basestring,)): |
def __init__(self, ignore_classes=(basestring,)): |
||||||
"""ignore_classes must include basestring, because if a string |
"""ignore_classes must include basestring, because if a string |
||||||
is iterable, so is a single character, and the routine runs |
is iterable, so is a single character, and the routine runs |
||||||
into an infinite recursion""" |
into an infinite recursion""" |
||||||
assert (basestring in ignore_classes, |
assert (basestring in ignore_classes, |
||||||
'basestring must be in ignore_classes') |
'basestring must be in ignore_classes') |
||||||
self.ignore_classes = ignore_classes |
self.ignore_classes = ignore_classes |
||||||
|
|
||||||
def __getitem__(self, candidate): |
def __getitem__(self, candidate): |
||||||
return dict.get(self, type(candidate)) or self._test(candidate) |
return dict.get(self, type(candidate)) or self._test(candidate) |
||||||
|
|
||||||
def _test(self, candidate): |
def _test(self, candidate): |
||||||
try: |
try: |
||||||
if isinstance(candidate, self.ignore_classes): |
if isinstance(candidate, self.ignore_classes): |
||||||
raise TypeError |
raise TypeError |
||||||
iter(candidate) |
iter(candidate) |
||||||
result = True |
result = True |
||||||
except TypeError: |
except TypeError: |
||||||
result = False |
result = False |
||||||
self[type(candidate)] = result |
self[type(candidate)] = result |
||||||
return result |
return result |
||||||
|
|
||||||
|
|
||||||
def iflatten(subject, test=None): |
def iflatten(subject, test=None): |
||||||
if test is None: |
if test is None: |
||||||
test = iterable_test() |
test = iterable_test() |
||||||
if not test[subject]: |
if not test[subject]: |
||||||
yield subject |
yield subject |
||||||
else: |
else: |
||||||
for elem in subject: |
for elem in subject: |
||||||
for subelem in iflatten(elem, test): |
for subelem in iflatten(elem, test): |
||||||
yield subelem |
yield subelem |
||||||
|
|
||||||
|
|
||||||
def flatten(subject, test=None): |
def flatten(subject, test=None): |
||||||
"""flatten an iterable with possible nested iterables. |
"""flatten an iterable with possible nested iterables. |
||||||
Adapted from |
Adapted from |
||||||
http://mail.python.org/pipermail/python-list/2003-November/233971.html |
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'] |
>>> flatten(['a','b',['c','d',['e','f'],'g'],'h']) == ['a','b','c','d','e','f','g','h'] |
||||||
True |
True |
||||||
|
|
||||||
Note this will normally ignore string types as iterables. |
Note this will normally ignore string types as iterables. |
||||||
>>> flatten(['ab', 'c']) |
>>> flatten(['ab', 'c']) |
||||||
['ab', 'c'] |
['ab', 'c'] |
||||||
""" |
""" |
||||||
return list(iflatten(subject, test)) |
return list(iflatten(subject, test)) |
||||||
|
@ -1,63 +1,63 @@ |
|||||||
#!python |
#!python |
||||||
|
|
||||||
import os |
import os |
||||||
import sys |
import sys |
||||||
from setuptools import find_packages |
from setuptools import find_packages |
||||||
|
|
||||||
from distutils.cmd import Command |
from distutils.cmd import Command |
||||||
|
|
||||||
|
|
||||||
class DisabledTestCommand(Command): |
class DisabledTestCommand(Command): |
||||||
user_options = [] |
user_options = [] |
||||||
|
|
||||||
def __init__(self, dist): |
def __init__(self, dist): |
||||||
raise RuntimeError( |
raise RuntimeError( |
||||||
"test command not supported on pygal." |
"test command not supported on pygal." |
||||||
" Use setup.py nosetests instead") |
" Use setup.py nosetests instead") |
||||||
|
|
||||||
_this_dir = os.path.dirname(__file__) |
_this_dir = os.path.dirname(__file__) |
||||||
_readme = os.path.join(_this_dir, 'readme.txt') |
_readme = os.path.join(_this_dir, 'readme.txt') |
||||||
_long_description = open(_readme).read().strip() |
_long_description = open(_readme).read().strip() |
||||||
|
|
||||||
# it seems that dateutil 2.0 only works under Python 3 |
# it seems that dateutil 2.0 only works under Python 3 |
||||||
dateutil_req = ( |
dateutil_req = ( |
||||||
['python-dateutil>=1.4,<2.0dev'] if sys.version_info < (3, 0) |
['python-dateutil>=1.4,<2.0dev'] if sys.version_info < (3, 0) |
||||||
else ['python-dateutil>=2.0']) |
else ['python-dateutil>=2.0']) |
||||||
|
|
||||||
setup_params = dict( |
setup_params = dict( |
||||||
name="pygal", |
name="pygal", |
||||||
description="Python svg graph abstract layer", |
description="Python svg graph abstract layer", |
||||||
long_description=_long_description, |
long_description=_long_description, |
||||||
author="Jason R. Coombs, Kozea", |
author="Jason R. Coombs, Kozea", |
||||||
author_email="jaraco@jaraco.com, gayoub@kozea.fr", |
author_email="jaraco@jaraco.com, gayoub@kozea.fr", |
||||||
url="https://github.com/Kozea/pygal", |
url="https://github.com/Kozea/pygal", |
||||||
packages=find_packages(), |
packages=find_packages(), |
||||||
zip_safe=True, |
zip_safe=True, |
||||||
include_package_data=True, |
include_package_data=True, |
||||||
install_requires=[ |
install_requires=[ |
||||||
'cssutils>=0.9.8a3', |
'cssutils>=0.9.8a3', |
||||||
'lxml>=2.0', |
'lxml>=2.0', |
||||||
] + dateutil_req, |
] + dateutil_req, |
||||||
license="MIT", |
license="MIT", |
||||||
classifiers=[ |
classifiers=[ |
||||||
"Development Status :: 5 - Production/Stable", |
"Development Status :: 5 - Production/Stable", |
||||||
"Intended Audience :: Developers", |
"Intended Audience :: Developers", |
||||||
"Intended Audience :: Science/Research", |
"Intended Audience :: Science/Research", |
||||||
"Programming Language :: Python :: 2.6", |
"Programming Language :: Python :: 2.6", |
||||||
"Programming Language :: Python :: 2.7", |
"Programming Language :: Python :: 2.7", |
||||||
"Programming Language :: Python :: 3", |
"Programming Language :: Python :: 3", |
||||||
"License :: OSI Approved :: MIT License", |
"License :: OSI Approved :: MIT License", |
||||||
], |
], |
||||||
entry_points={ |
entry_points={ |
||||||
}, |
}, |
||||||
# Don't use setup.py test - nose doesn't support it |
# Don't use setup.py test - nose doesn't support it |
||||||
# see http://code.google.com/p/python-nose/issues/detail?id=219 |
# see http://code.google.com/p/python-nose/issues/detail?id=219 |
||||||
cmdclass=dict( |
cmdclass=dict( |
||||||
test=DisabledTestCommand, |
test=DisabledTestCommand, |
||||||
), |
), |
||||||
use_2to3=True, |
use_2to3=True, |
||||||
) |
) |
||||||
|
|
||||||
if __name__ == '__main__': |
if __name__ == '__main__': |
||||||
from setuptools import setup |
from setuptools import setup |
||||||
setup(**setup_params) |
setup(**setup_params) |
||||||
|
Loading…
Reference in new issue