Browse Source

Began Schedule implementation (it's still riddled with and design problems

pull/8/head
jaraco 17 years ago
parent
commit
42255d7649
  1. 8
      lib/SVG/Plot.py
  2. 368
      lib/SVG/Schedule.py
  3. 2
      lib/SVG/__init__.py
  4. 49
      lib/SVG/util.py
  5. 38
      test/testing.py
  6. 45
      test/testing.rb

8
lib/SVG/Plot.py

@ -2,16 +2,12 @@
import SVG import SVG
from itertools import izip, count, chain from itertools import izip, count, chain
from util import float_range
def get_pairs(i): def get_pairs(i):
i = iter(i) i = iter(i)
while True: yield i.next(), i.next() while True: yield i.next(), i.next()
def float_range(start = 0, stop = None, step = 1):
"Much like the built-in function range, but accepts floats"
while start < stop:
yield float(start)
start += step
class Plot(SVG.Graph): class Plot(SVG.Graph):
"""=== For creating SVG plots of scalar data """=== For creating SVG plots of scalar data

368
lib/SVG/Schedule.py

@ -0,0 +1,368 @@
#!python
import re
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta
from SVG import Graph
from util import grouper, date_range
__all__ = ('Schedule')
class Schedule(Graph):
"""
# === For creating SVG plots of scalar temporal data
= Synopsis
require 'SVG/Graph/Schedule'
# Data sets are label, start, end tripples.
data1 = [
"Housesitting", "6/17/04", "6/19/04",
"Summer Session", "6/15/04", "8/15/04",
]
graph = SVG::Graph::Schedule.new( {
:width => 640,
:height => 480,
:graph_title => title,
:show_graph_title => true,
:no_css => true,
:scale_x_integers => true,
:scale_y_integers => true,
:min_x_value => 0,
:min_y_value => 0,
:show_data_labels => true,
:show_x_guidelines => true,
:show_x_title => true,
:x_title => "Time",
:stagger_x_labels => true,
:stagger_y_labels => true,
:x_label_format => "%m/%d/%y",
})
graph.add_data({
:data => data1,
:title => 'Data',
})
print graph.burn()
= Description
Produces a graph of temporal scalar data.
= Examples
http://www.germane-software/repositories/public/SVG/test/schedule.rb
= Notes
The default stylesheet handles upto 10 data sets, if you
use more you must create your own stylesheet and add the
additional settings for the extra data sets. You will know
if you go over 10 data sets as they will have no style and
be in black.
Note that multiple data sets within the same chart can differ in
length, and that the data in the datasets needn't be in order;
they will be ordered by the plot along the X-axis.
The dates must be parseable by ParseDate, but otherwise can be
any order of magnitude (seconds within the hour, or years)
= See also
* SVG::Graph::Graph
* SVG::Graph::BarHorizontal
* SVG::Graph::Bar
* SVG::Graph::Line
* SVG::Graph::Pie
* SVG::Graph::Plot
* SVG::Graph::TimeSeries
== Author
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom>
Copyright 2004 Sean E. Russell
This software is available under the Ruby license[LICENSE.txt]
"""
"The format string to be used to format the X axis labels"
x_label_format = '%Y-%m-%d %H:%M:%S'
"""
Use this to set the spacing between dates on the axis. The value
must be of the form
"\d+ ?((year|month|week|day|hour|minute|second)s?)?"
e.g.
graph.timescale_divisions = '2 weeks'
graph.timescale_divisions = '1 month'
graph.timescale_divisions = '3600 seconds' # easier would be '1 hour'
"""
timescale_divisions = None
"The formatting used for the popups. See x_label_format"
popup_format = '%Y-%m-%d %H:%M:%S'
_min_x_value = None
scale_x_divisions = False
scale_x_integers = False
bar_gap = True
def add_data(self, data):
"""
Add data to the plot.
# A data set with 1 point: Lunch from 12:30 to 14:00
d1 = [ "Lunch", "12:30", "14:00" ]
# A data set with 2 points: "Cats" runs from 5/11/03 to 7/15/04, and
# "Henry V" runs from 6/12/03 to 8/20/03
d2 = [ "Cats", "5/11/03", "7/15/04",
"Henry V", "6/12/03", "8/20/03" ]
graph.add_data(
:data => d1,
:title => 'Meetings'
)
graph.add_data(
:data => d2,
:title => 'Plays'
)
Note that the data must be in time,value pairs, and that the date format
may be any date that is parseable by ParseDate.
Also note that, in this example, we're mixing scales; the data from d1
will probably not be discernable if both data sets are plotted on the same
graph, since d1 is too granular.
"""
# note, the only reason this method is overridden is to change the
# docstring
super(Schedule, self).add_data(data)
# copied from Bar
# TODO, refactor this into a common base class (or mix-in)
def get_bar_gap(self, field_size):
bar_gap = 10 # default gap
if field_size < 10:
# adjust for narrow fields
bar_gap = field_size / 2
# the following zero's out the gap if bar_gap is False
bar_gap = int(self.bar_gap) * bar_gap
return bar_gap
def validate_data(self, conf):
super(Schedule, self).validate_data(conf)
msg = "Data supplied must be (title, from, to) tripples!"
assert len(conf['data']) % 3 == 0, msg
def process_data(self, conf):
super(Schedule, self).process_data(conf)
data = conf['data']
triples = grouper(3, data)
begin_dates, end_dates, labels = zip(*triples)
begin_dates = map(self.parse_date, begin_dates)
end_dates = map(self.parse_date, end_dates)
# reconstruct the triples in a new order
reordered_triples = zip(begin_dates, end_dates, labels)
reordered_triples.sort()
conf['data'] = reordered_triples
def parse_date(self, date_string):
print 'attempting to parse %s as date' % date_string
return parse(date_string)
def set_min_x_value(self, value):
if isinstance(value, basestring):
value = self.parse_date(value)
self._min_x_value = value
def get_min_x_value(self):
return self._min_x_value
min_x_value = property(get_min_x_value, set_min_x_value)
def format(self, x, y):
return x.strftime(self.popup_format)
def get_x_labels(self):
format = lambda x: x.strftime(self.x_label_format)
return map(format, self.get_x_values())
def y_label_offset(self, height):
return height / -2.0
def get_y_labels(self):
data = self.data['data']
begin_dates, start_dates, labels = zip(*data)
return labels
def draw_data(self):
bar_gap = self.get_bar_gap()
subbar_height = self.field_height - bar_gap
y_mod = (subbar_height / 2) + (font_size / 2)
x_min,x_max,div = self.x_range()
x_range = x_max - x_min
scale = (float(self.graph_width) - self.font_size*2)
scale /= x_range
for index, (x_start, x_end, label) in enumerate(self.data['data']):
count = index + 1 # index is 0-based, count is 1-based
y = self.graph_height - (self.field_height*count)
bar_width = (x_end-x_stant)*scale
bar_start = (x_start-x_min)*scale
rect = self._create_element('rect', {
'x': str(bar_start),
'y': str(y),
'width': str(bar_width),
'height': str(subbar_height),
'class': 'fill%s' % (count+1), # TODO: doublecheck that +1 is correct (that's what's in the Ruby code)
})
self.graph.appendChild(rect)
def get_css(self):
return """\
/* default fill styles for multiple datasets (probably only use a single dataset on this graph though) */
.key1,.fill1{
fill: #ff0000;
fill-opacity: 0.5;
stroke: none;
stroke-width: 0.5px;
}
.key2,.fill2{
fill: #0000ff;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key3,.fill3{
fill: #00ff00;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key4,.fill4{
fill: #ffcc00;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key5,.fill5{
fill: #00ccff;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key6,.fill6{
fill: #ff00ff;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key7,.fill7{
fill: #00ffff;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key8,.fill8{
fill: #ffff00;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key9,.fill9{
fill: #cc6666;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key10,.fill10{
fill: #663399;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key11,.fill11{
fill: #339900;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
.key12,.fill12{
fill: #9966FF;
fill-opacity: 0.5;
stroke: none;
stroke-width: 1px;
}
"""
def _x_range(self):
start_dates, end_dates, labels = zip(*self.data['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 = relativedelta(max_value, min_value)
right_pad = (range / 20.0) or relativedelta(days=10)
scale_range = (max_value + right_pad) - min_value
scale_division = self.scale_x_divisions or (scale_range / 10.0)
# this doesn't make sense, because x is a timescale
#if self.scale_x_integers:
# scale_division = min(round(scale_division), 1)
return min_value, max_value, scale_division
def get_x_values(self):
x_min, x_max, scale_division = self._x_range()
if timescale_divisions:
pattern = re.compile('(\d+) ?(\w+)')
m = pattern.match(timescale_divisions)
if not m:
raise ValueError, "Invalid timescale_divisions: %s" % timescale_divisions
magnitude = int(m.groups(1))
units = m.groups(2)
parameter = self.lookup_relativedelta_parameter(units)
delta = relativedelta(**{parameter:magnitude})
return daterange(x_min, x_max, delta)
else:
# I think much of the code is assuming x is an integer (seconds)
# The whole logic needs to be revisited for this purpose.
return range(x_min, x_max, scale_division)
def lookup_relativedelta_parameter(self, unit_string):
from util import reverse_mapping, flatten_mapping
unit_string = unit_string.lower()
mapping = dict(
years = ('years', 'year', 'yrs', 'yr'),
months = ('months', 'month', 'mo'),
weeks = ('weeks', 'week', 'wks' ,'wk'),
days = ('days', 'day'),
hours = ('hours', 'hour', 'hr', 'hrs', 'h'),
minutes = ('minutes', 'minute', 'min', 'mins', 'm'),
seconds = ('seconds', 'second', 'sec', 'secs', 's'),
)
mapping = reverse_mapping(mapping)
mapping = flatten_mapping(mapping)
if not unit_string in mapping:
raise ValueError, "%s doesn't match any supported time/date unit"
return mapping[unit_string]

2
lib/SVG/__init__.py

@ -1,7 +1,7 @@
#!python #!python
# -*- coding: UTF-8 -*- # -*- coding: UTF-8 -*-
__all__ = ('Plot', 'TimeSeries', 'Bar', 'Pie') __all__ = ('Plot', 'TimeSeries', 'Bar', 'Pie', 'Schedule')
from xml.dom import minidom as dom from xml.dom import minidom as dom
from operator import itemgetter from operator import itemgetter

49
lib/SVG/util.py

@ -0,0 +1,49 @@
#!python
from itertools import chain, repeat, izip
# from itertools recipes (python documentation)
def grouper(n, iterable, padvalue=None):
"grouper(3, 'abcdefg', 'x') --> ('a','b','c'), ('d','e','f'), ('g','x','x')"
return izip(*[chain(iterable, repeat(padvalue, n-1))]*n)
def reverse_mapping(mapping):
"""
For every key, value pair, return the mapping for the
equivalent value, key pair
"""
keys, values = mapping.items()
return dict(zip(values, keys))
def flatten_mapping(mapping):
"""
For every key that has an __iter__ method, assign the values
to a key for each.
>>> flatten_mapping({'ab': 3, ('c','d'): 4}) == {'ab': 3, 'c': 4, 'd': 4}
True
"""
return dict(flatten_items(mapping.items()))
def flatten_items(items):
for keys, value in items:
if hasattr(keys, '__iter__'):
for key in keys:
yield (key, value)
else:
yield (keys, value)
def float_range(start=0, stop=None, step=1):
"Much like the built-in function range, but accepts floats"
start = float(start)
while start < stop:
yield start
start += step
def date_range(start=None, stop=None, step=None):
"Much like the built-in function range, but works with floats"
if step is None: step = relativedelta(days=1)
if start is None: start = datetime.datetime.now()
while start < stop:
yield start
start += step

38
test/testing.py

@ -93,3 +93,41 @@ g.add_data({'data': [-2, 3, 1, 3, 1], 'title': 'Female'})
g.add_data({'data': [0, 2, 1, 5, 4], 'title': 'Male'}) g.add_data({'data': [0, 2, 1, 5, 4], 'title': 'Male'})
open('Pie.py.svg', 'w').write(g.burn()) open('Pie.py.svg', 'w').write(g.burn())
from SVG import Schedule
title = "Billy's Schedule"
data1 = [
"History 107", "5/19/04", "6/30/04",
"Algebra 011", "6/2/04", "8/11/04",
"Psychology 101", "6/28/04", "8/9/04",
"Acting 105", "7/7/04", "8/16/04"
]
g = Schedule.Schedule(dict(
width = 640,
height = 480,
graph_title = title,
show_graph_title = True,
key = False,
scale_x_integers = True,
scale_y_integers = True,
show_data_labels = True,
show_y_guidelines = False,
show_x_guidelines = True,
show_x_title = True,
x_title = "Time",
show_y_title = False,
rotate_x_labels = True,
rotate_y_labels = False,
x_label_format = "%m/%d",
timescale_divisions = "1 week",
add_popups = True,
popup_format = "%m/%d/%y",
area_fill = True,
min_y_value = 0,
))
g.add_data(dict(data=data1, title="Data"))
f = open('Schedule.py.svg', 'w').write(g.burn())

45
test/testing.rb

@ -132,3 +132,48 @@ g.add_data({:data=>[0,2,1,5,4], :title=>'Male'})
f = File.new('Line.rb.svg', 'w') f = File.new('Line.rb.svg', 'w')
f.write(g.burn()) f.write(g.burn())
f.close() f.close()
require 'SVG/Graph/Schedule'
title = "Billy's Schedule"
data1 = [
"History 107", "5/19/04", "6/30/04",
"Algebra 011", "6/2/04", "8/11/04",
"Psychology 101", "6/28/04", "8/9/04",
"Acting 105", "7/7/04", "8/16/04"
]
g = SVG::Graph::Schedule.new( {
:width => 640,
:height => 480,
:graph_title => title,
:show_graph_title => true,
# :no_css => true,
:key => false,
:scale_x_integers => true,
:scale_y_integers => true,
:show_data_labels => true,
:show_y_guidelines => false,
:show_x_guidelines => true,
:show_x_title => true,
:x_title => "Time",
:show_y_title => false,
:rotate_x_labels => true,
:rotate_y_labels => false,
:x_label_format => "%m/%d",
# :timescale_divisions => "1 weeks",
:add_popups => true,
:popup_format => "%m/%d/%y",
:area_fill => true,
:min_y_value => 0,
})
g.add_data(
:data => data1,
:title => "Data"
)
f = File.new('Schedule.rb.svg', 'w')
f.write(g.burn())
f.close()

Loading…
Cancel
Save