Python to generate nice looking SVG graph http://pygal.org/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

314 lines
10 KiB

#!python
import re
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta
from lxml import etree
from svg.charts.graph import Graph
from util import grouper, date_range, divide_timedelta_float, TimeScale
__all__ = ('Schedule')
14 years ago
class Schedule(Graph):
"""
# === For creating SVG plots of scalar temporal data
14 years ago
= Synopsis
14 years ago
require 'SVG/Graph/Schedule'
14 years ago
# Data sets are label, start, end tripples.
data1 = [
14 years ago
"Housesitting", "6/17/04", "6/19/04",
"Summer Session", "6/15/04", "8/15/04",
]
14 years ago
graph = SVG::Graph::Schedule.new( {
:width => 640,
:height => 480,
:graph_title => title,
:show_graph_title => true,
:no_css => true,
:scale_x_integers => true,
:scale_y_integers => true,
:min_x_value => 0,
:min_y_value => 0,
:show_data_labels => true,
:show_x_guidelines => true,
:show_x_title => true,
:x_title => "Time",
:stagger_x_labels => true,
:stagger_y_labels => true,
:x_label_format => "%m/%d/%y",
})
14 years ago
graph.add_data({
:data => data1,
:title => 'Data',
})
14 years ago
print graph.burn()
14 years ago
= Description
14 years ago
Produces a graph of temporal scalar data.
14 years ago
= Examples
14 years ago
http://www.germane-software/repositories/public/SVG/test/schedule.rb
14 years ago
= Notes
14 years ago
The default stylesheet handles upto 10 data sets, if you
use more you must create your own stylesheet and add the
additional settings for the extra data sets. You will know
if you go over 10 data sets as they will have no style and
be in black.
14 years ago
Note that multiple data sets within the same chart can differ in
length, and that the data in the datasets needn't be in order;
they will be ordered by the plot along the X-axis.
14 years ago
The dates must be parseable by ParseDate, but otherwise can be
any order of magnitude (seconds within the hour, or years)
14 years ago
= See also
14 years ago
* SVG::Graph::Graph
* SVG::Graph::BarHorizontal
* SVG::Graph::Bar
* SVG::Graph::Line
* SVG::Graph::Pie
* SVG::Graph::Plot
* SVG::Graph::TimeSeries
14 years ago
== Author
14 years ago
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
14 years ago
Copyright 2004 Sean E. Russell
This software is available under the Ruby license[LICENSE.txt]
14 years ago
"""
14 years ago
"The format string to be used to format the X axis labels"
x_label_format = '%Y-%m-%d %H:%M:%S'
14 years ago
"""
Use this to set the spacing between dates on the axis. The value
must be of the form
"\d+ ?((year|month|week|day|hour|minute|second)s?)?"
14 years ago
e.g.
14 years ago
graph.timescale_divisions = '2 weeks'
graph.timescale_divisions = '1 month'
graph.timescale_divisions = '3600 seconds' # easier would be '1 hour'
"""
timescale_divisions = None
14 years ago
"The formatting used for the popups. See x_label_format"
popup_format = '%Y-%m-%d %H:%M:%S'
_min_x_value = None
scale_x_divisions = False
scale_x_integers = False
bar_gap = True
stylesheet_names = Graph.stylesheet_names + ['bar.css']
def add_data(self, data):
"""
Add data to the plot.
14 years ago
# A data set with 1 point: Lunch from 12:30 to 14:00
14 years ago
d1 = [ "Lunch", "12:30", "14:00" ]
# A data set with 2 points: "Cats" runs from 5/11/03 to 7/15/04, and
# "Henry V" runs from 6/12/03 to 8/20/03
d2 = [ "Cats", "5/11/03", "7/15/04",
"Henry V", "6/12/03", "8/20/03" ]
14 years ago
graph.add_data(
:data => d1,
:title => 'Meetings'
)
graph.add_data(
:data => d2,
:title => 'Plays'
)
14 years ago
Note that the data must be in time,value pairs,
and that the date format
may be any date that is parseable by ParseDate.
Also note that, in this example, we're mixing scales; the data from d1
14 years ago
will probably not be discernable if both data sets are
plotted on the same graph, since d1 is too granular.
"""
# The ruby version does something different here, throwing out
# any previously added data.
super(Schedule, self).add_data(data)
# copied from Bar
# TODO, refactor this into a common base class (or mix-in)
def get_bar_gap(self, field_size):
14 years ago
bar_gap = 10 # default gap
if field_size < 10:
# adjust for narrow fields
bar_gap = field_size / 2
# the following zero's out the gap if bar_gap is False
bar_gap = int(self.bar_gap) * bar_gap
return bar_gap
def validate_data(self, conf):
super(Schedule, self).validate_data(conf)
msg = "Data supplied must be (title, from, to) tripples!"
assert len(conf['data']) % 3 == 0, msg
def process_data(self, conf):
super(Schedule, self).process_data(conf)
data = conf['data']
triples = grouper(3, data)
14 years ago
labels, begin_dates, end_dates = zip(*triples)
14 years ago
begin_dates = map(self.parse_date, begin_dates)
end_dates = map(self.parse_date, end_dates)
# reconstruct the triples in a new order
reordered_triples = zip(begin_dates, end_dates, labels)
14 years ago
# because of the reordering, this will sort by begin_date
# then end_date, then label.
reordered_triples.sort()
14 years ago
conf['data'] = reordered_triples
def parse_date(self, date_string):
return parse(date_string)
14 years ago
def set_min_x_value(self, value):
if isinstance(value, basestring):
value = self.parse_date(value)
self._min_x_value = value
def get_min_x_value(self):
return self._min_x_value
14 years ago
min_x_value = property(get_min_x_value, set_min_x_value)
14 years ago
def format(self, x, y):
return x.strftime(self.popup_format)
14 years ago
def get_x_labels(self):
format = lambda x: x.strftime(self.x_label_format)
return map(format, self.get_x_values())
14 years ago
def y_label_offset(self, height):
return height / -2.0
14 years ago
def get_y_labels(self):
# ruby version uses the last data supplied
last = -1
data = self.data[last]['data']
begin_dates, start_dates, labels = zip(*data)
return labels
14 years ago
def draw_data(self):
bar_gap = self.get_bar_gap(self.get_field_height())
14 years ago
subbar_height = self.get_field_height() - bar_gap
14 years ago
y_mod = (subbar_height / 2) + (self.font_size / 2)
14 years ago
x_min, x_max, div = self._x_range()
x_range = x_max - x_min
14 years ago
width = (float(self.graph_width) - self.font_size * 2)
# time_scale
#scale /= x_range
scale = TimeScale(width, x_range)
14 years ago
# ruby version uses the last data supplied
last = -1
data = self.data[last]['data']
14 years ago
for index, (x_start, x_end, label) in enumerate(data):
14 years ago
count = index + 1 # index is 0-based, count is 1-based
y = self.graph_height - (self.get_field_height() * count)
bar_width = scale * (x_end - x_start)
bar_start = scale * (x_start - x_min)
etree.SubElement(self.graph, 'rect', {
'x': str(bar_start),
'y': str(y),
'width': str(bar_width),
'height': str(subbar_height),
14 years ago
'class': 'fill%s' % (count + 1),
})
def _x_range(self):
# ruby version uses teh last data supplied
last = -1
data = self.data[last]['data']
14 years ago
start_dates, end_dates, labels = zip(*data)
all_dates = start_dates + end_dates
max_value = max(all_dates)
if not self.min_x_value is None:
all_dates.append(self.min_x_value)
min_value = min(all_dates)
range = max_value - min_value
14 years ago
right_pad = divide_timedelta_float(
range, 20.0) or relativedelta(days=10)
scale_range = (max_value + right_pad) - min_value
14 years ago
#scale_division = self.scale_x_divisions or (scale_range / 10.0)
# todo, remove timescale_x_divisions and use scale_x_divisions only
# but as a time delta
scale_division = divide_timedelta_float(scale_range, 10.0)
14 years ago
# this doesn't make sense, because x is a timescale
#if self.scale_x_integers:
# scale_division = min(round(scale_division), 1)
14 years ago
return min_value, max_value, scale_division
14 years ago
def get_x_values(self):
x_min, x_max, scale_division = self._x_range()
if self.timescale_divisions:
pattern = re.compile('(\d+) ?(\w+)')
m = pattern.match(self.timescale_divisions)
if not m:
14 years ago
raise (ValueError,
"Invalid timescale_divisions: %s" %
self.timescale_divisions)
magnitude = int(m.group(1))
units = m.group(2)
14 years ago
parameter = self.lookup_relativedelta_parameter(units)
14 years ago
delta = relativedelta(**{parameter: magnitude})
scale_division = delta
return date_range(x_min, x_max, scale_division)
def lookup_relativedelta_parameter(self, unit_string):
from util import reverse_mapping, flatten_mapping
unit_string = unit_string.lower()
mapping = dict(
14 years ago
years=('years', 'year', 'yrs', 'yr'),
months=('months', 'month', 'mo'),
weeks=('weeks', 'week', 'wks', 'wk'),
days=('days', 'day'),
hours=('hours', 'hour', 'hr', 'hrs', 'h'),
minutes=('minutes', 'minute', 'min', 'mins', 'm'),
seconds=('seconds', 'second', 'sec', 'secs', 's'),
)
mapping = reverse_mapping(mapping)
mapping = flatten_mapping(mapping)
if not unit_string in mapping:
14 years ago
raise ValueError("%s doesn't match any supported time/date unit")
return mapping[unit_string]