Browse Source

Fixed more bugs. Testing now shows high correlation with Ruby code for Plot.

Fixed some bugs in TimeSeries, and re-wired the date-handling so it now uses integers for the x-axis.  These may be converted back to date/time later, but not unless the datetime module can handle timedelta division correctly.  Even the dateutil module doesn't handle this.
pull/8/head
SANDIA\jaraco 19 years ago
parent
commit
a400141f0d
  1. 14
      Plot.py
  2. 52
      TimeSeries.py
  3. 64
      __init__.py
  4. 27
      testing.py
  5. 32
      testing.rb

14
Plot.py

@ -9,7 +9,7 @@ def get_pairs( i ):
def float_range( start = 0, stop = None, step = 1 ):
"Much like the built-in function range, but accepts floats"
while start < stop:
yield start
yield float( start )
start += step
class Plot( SVG.Graph ):
@ -145,12 +145,12 @@ class Plot( SVG.Graph ):
data['data'] = zip( *pairs )
def calculate_left_margin( self ):
super( self.__class__, self ).calculate_left_margin()
super( Plot, self ).calculate_left_margin()
label_left = len( str( self.get_x_labels()[0] ) ) / 2 * self.font_size * 0.6
self.border_left = max( label_left, self.border_left )
def calculate_right_margin( self ):
super( self.__class__, self ).calculate_right_margin()
super( Plot, self ).calculate_right_margin()
label_right = len( str( self.get_x_labels()[-1] ) ) / 2 * self.font_size * 0.6
self.border_right = max( label_right, self.border_right )
@ -166,6 +166,7 @@ class Plot( SVG.Graph ):
min_value = min( min_value, spec_min )
range = max_value - min_value
#side_pad = '%s_pad' % side
side_pad = range / 20.0 or 10
scale_range = ( max_value + side_pad ) - min_value
@ -204,7 +205,6 @@ class Plot( SVG.Graph ):
side_align = getattr( self, '%s_align' % side )
result = ( float( graph_size ) - self.font_size*2*side_font ) / \
( len( values ) + dx - side_align )
for key,val in vars().items(): print key, val
return result
def field_width( self ): return self.field_size( 'x' )
@ -223,11 +223,11 @@ class Plot( SVG.Graph ):
if self.area_fill:
graph_height = self.graph_height
path = self._create_element( 'path', {
'd': 'M%(x_start)s %(graph_height)d %(lpath)s V%(graph_height)d Z' % vars(),
'd': 'M%(x_start)f %(graph_height)f %(lpath)s V%(graph_height)f Z' % vars(),
'class': 'fill%(line)d' % vars() } )
self.graph.appendChild( path )
path = self._create_element( 'path', {
'd': 'M%(x_start)d %(y_start)d %(lpath)s' % vars(),
'd': 'M%(x_start)f %(y_start)f %(lpath)s' % vars(),
'class': 'line%(line)d' % vars() } )
self.graph.appendChild( path )
self.draw_data_points( line, data_points, graph_points )
@ -248,7 +248,7 @@ class Plot( SVG.Graph ):
return map( self.transform_output_coordinates, data_points )
def get_lpath( self, points ):
points = map( lambda p: "%d %d" % p, points )
points = map( lambda p: "%f %f" % p, points )
return 'L' + ' '.join( points )
def transform_output_coordinates( self, (x,y) ):

52
TimeSeries.py

@ -1,5 +1,12 @@
#!/usr/bin/env python
import SVG
import re
#requires python date-util from http://labix.org/python-dateutil
from dateutil.parser import parse
from dateutil.relativedelta import relativedelta
from time import mktime
import datetime
fromtimestamp = datetime.datetime.fromtimestamp
class Plot( SVG.Plot.Plot ):
"""=== For creating SVG plots of scalar temporal data
@ -98,6 +105,18 @@ class Plot( SVG.Plot.Plot ):
__doc_popup_format_ = "The formatting usped for the popups. See x_label_format"
__doc_x_label_format_ = "The format string used to format the X axis labels. See strftime."
timescale_divisions = None
__doc_timescale_divisions_ = """Use this to set the spacing between dates on the axis. The value
must be of the form
"\d+ ?(days|weeks|months|years|hours|minutes|seconds)?"
EG:
graph.timescale_divisions = "2 weeks"
will cause the chart to try to divide the X axis up into segments of
two week periods."""
def add_data( self, data ):
"""Add data to the plot.
d1 = [ "12:30", 2 ] # A data set with 1 point: ("12:30",2)
@ -114,13 +133,14 @@ class Plot( SVG.Plot.Plot ):
Note that the data must be in time,value pairs, and that the date format
may be any date that is parseable by ParseDate."""
super( self.__class__, self ).add_data( data )
super( Plot, self ).add_data( data )
def process_data( self, data ):
super( self.__class__, self ).process_data( data )
super( Plot, self ).process_data( data )
# the date should be in the first element, so parse it out
data['data'][0] = map( self.parse_date, data['data'][0] )
_min_x_value = SVG.Plot.Plot.min_x_value
def get_min_x_value( self ):
return self._min_x_value
def set_min_x_value( self, date ):
@ -128,38 +148,34 @@ class Plot( SVG.Plot.Plot ):
min_x_value = property( get_min_x_value, set_min_x_value )
def format( self, x, y ):
return x.strftime( self.popup_format )
return fromtimestamp( x ).strftime( self.popup_format )
def get_x_labels( self ):
return map( lambda t: t.strftime( self.x_label_format ), self.get_x_values() )
return map( lambda t: fromtimestamp( t ).strftime( self.x_label_format ), self.get_x_values() )
def get_x_values( self ):
result = self.get_x_timescale_division_values()
if result: return result
return range( *self.x_range() )
return SVG.Plot.float_range( *self.x_range() )
def get_x_timescale_division_values( self ):
if not self.timescale_divisions: return
min, max, scale_division = self.x_range()
m = re.match( '(?P<amount>\d+) ?(?P<division_units>days|weeks|months|years|hours|minutes|seconds)?', self.timescale_divisions )
# copy amount and division_units into the local namespace
vars.update( m.groupdict() )
division_units = division_units or 'days'
amount = int( amount )
division_units = m.groupdict()['division_units'] or 'days'
amount = int( m.groupdict()['amount'] )
if not amount: return
if division_units == 'weeks':
amount *= 7
division_units = 'days'
# strip off the plural (s)
division_units = division_units[:-1]
result = self.get_time_range( min, max, step, units )
delta = relativedelta( **{ division_units: amount } )
result = self.get_time_range( min, max, delta )
return result
def get_time_range( self, start, stop, step, units ):
def get_time_range( self, start, stop, delta ):
start, stop = map( fromtimestamp, (start, stop ) )
current = start
while current < stop:
yield current
current.replace( **{ units: current.getattr( units ) + step } )
yield mktime( current.timetuple() )
current += delta
def parse_date( self, date_string ):
return strptime( date_string, '%Y-%m-%dT%H:%M:%S' )
return mktime( parse( date_string ).timetuple() )

64
__init__.py

@ -110,14 +110,14 @@ Copyright 2005 Sandia National Laboratories
top_align = top_font = right_align = right_font = 0
def __init__( self, config ):
def __init__( self, config = {} ):
"""Initialize the graph object with the graph settings. You won't
instantiate this class directly; see the subclass for options."""
self.load_config( config )
self.clear_data()
def load_config( self, config = None ):
if not config: config = self.defaults
map( lambda pair: setattr( self, *pair ), config.items() )
def load_config( self, config ):
self.__dict__.update( config )
def add_data( self, conf ):
"""This method allows you do add data to the graph object.
@ -314,7 +314,14 @@ Copyright 2005 Sandia National Laboratories
'class': 'dataPointLabel',
'style': '%(style)s stroke: #fff; stroke-width: 2;' % vars(),
} )
e.nodeValue = value
e.appendChild( self._doc.createTextNode( str( value ) ) )
self.foreground.appendChild( e )
e = self._create_element( 'text', {
'x': str( x ),
'y': str( y ),
'class': 'dataPointLabel' } )
e.appendChild( self._doc.createTextNode( str( value ) ) )
if style: e.setAttribute( 'style', style )
self.foreground.appendChild( e )
def draw_x_labels( self ):
@ -345,7 +352,7 @@ Copyright 2005 Sandia National Laboratories
y += stagger
graph_height = self.graph_height
path = self._create_element( 'path', {
'd': 'M%(x)d %(graph_height)d v%(stagger)d' % vars(),
'd': 'M%(x)f %(graph_height)f v%(stagger)d' % vars(),
'class': 'staggerGuideLine'
} )
self.graph.appendChild( path )
@ -391,7 +398,7 @@ Copyright 2005 Sandia National Laboratories
def get_y_offset( self ):
#result = self.graph_height + self.y_label_offset( label_height )
result = self.graph_height + self.y_label_offset( self.field_height() )
if self.rotate_y_labels: result += self.font_size/1.2
if not self.rotate_y_labels: result += self.font_size/1.2
return result
y_offset = property( get_y_offset )
@ -405,11 +412,11 @@ Copyright 2005 Sandia National Laboratories
y = self.y_offset - ( label_height * index )
x = {True: 0, False:-3}[self.rotate_y_labels]
if self.stagger_x_labels and (index % 2 ):
if self.stagger_y_labels and (index % 2 ):
stagger = self.y_label_font_size + 5
x -= stagger
path = self._create_element( 'path', {
'd': 'M%(x)d %(y)d v%(stagger)d' % vars(),
'd': 'M%(x)f %(y)f h%(stagger)d' % vars(),
'class': 'staggerGuideLine'
} )
self.graph.appendChild( path )
@ -424,10 +431,11 @@ Copyright 2005 Sandia National Laboratories
text.setAttribute( 'style', 'text-anchor: middle' )
else:
text.setAttribute( 'y', str( y - self.y_label_font_size/2 ) )
text.setAttribute( 'style', 'text-anchor: middle' )
text.setAttribute( 'style', 'text-anchor: end' )
def draw_x_guidelines( self, label_height, count ):
"Draw the X-axis guidelines"
if not self.show_x_guidelines: return
# skip the first one
for count in range(1,count):
start = label_height*count
@ -440,11 +448,12 @@ Copyright 2005 Sandia National Laboratories
def draw_y_guidelines( self, label_height, count ):
"Draw the Y-axis guidelines"
if not self.show_y_guidelines: return
for count in range( 1, count ):
start = self.graph_height - label_height*count
stop = self.graph_width
path = self._create_element( 'path', {
'd': 'MO %(start)s h%(stop)s' % vars(),
'd': 'M 0 %(start)s h%(stop)s' % vars(),
'class': 'guideLines' } )
self.graph.appendChild( path )
@ -582,7 +591,7 @@ Copyright 2005 Sandia National Laboratories
self.graph_height = self.height - self.border_top - self.border_bottom
def get_style( self ):
return """/* Copy from here for external style sheet */
result = """/* Copy from here for external style sheet */
.svgBackground{
fill:#ffffff;
}
@ -594,14 +603,14 @@ Copyright 2005 Sandia National Laboratories
.mainTitle{
text-anchor: middle;
fill: #000000;
font-size: #{title_font_size}px;
font-size: %(title_font_size)dpx;
font-family: "Arial", sans-serif;
font-weight: normal;
}
.subTitle{
text-anchor: middle;
fill: #999999;
font-size: #{subtitle_font_size}px;
font-size: %(subtitle_font_size)dpx;
font-family: "Arial", sans-serif;
font-weight: normal;
}
@ -620,7 +629,7 @@ Copyright 2005 Sandia National Laboratories
.xAxisLabels{
text-anchor: middle;
fill: #000000;
font-size: #{x_label_font_size}px;
font-size: %(x_label_font_size)dpx;
font-family: "Arial", sans-serif;
font-weight: normal;
}
@ -628,7 +637,7 @@ Copyright 2005 Sandia National Laboratories
.yAxisLabels{
text-anchor: end;
fill: #000000;
font-size: #{y_label_font_size}px;
font-size: %(y_label_font_size)dpx;
font-family: "Arial", sans-serif;
font-weight: normal;
}
@ -636,7 +645,7 @@ Copyright 2005 Sandia National Laboratories
.xAxisTitle{
text-anchor: middle;
fill: #ff0000;
font-size: #{x_title_font_size}px;
font-size: %(x_title_font_size)dpx;
font-family: "Arial", sans-serif;
font-weight: normal;
}
@ -644,7 +653,7 @@ Copyright 2005 Sandia National Laboratories
.yAxisTitle{
fill: #ff0000;
text-anchor: middle;
font-size: #{y_title_font_size}px;
font-size: %(y_title_font_size)dpx;
font-family: "Arial", sans-serif;
font-weight: normal;
}
@ -663,17 +672,19 @@ Copyright 2005 Sandia National Laboratories
stroke-width: 0.5px;
}
%s
%%s
.keyText{
fill: #000000;
text-anchor:start;
font-size: #{key_font_size}px;
font-size: %(key_font_size)dpx;
font-family: "Arial", sans-serif;
font-weight: normal;
}
/* End copy for external style sheet */
""" % self.get_css()
""" % class_dict( self )
result = result % self.get_css()
return result
def _create_element( self, nodeName, attributes={} ):
"Create an XML node and set the attributes from a dict"
@ -681,3 +692,14 @@ Copyright 2005 Sandia National Laboratories
map( lambda a: node.setAttribute( *a ), attributes.items() )
return node
class class_dict( object ):
"Emulates a dictionary, but retrieves class attributes"
def __init__( self, obj ):
self.__obj__ = obj
def __getitem__( self, item ):
return getattr( self.__obj__, item )
def keys( self ):
# dir returns a good guess of what attributes might be available
return dir( self.__obj__ )

27
testing.py

@ -4,10 +4,31 @@ import SVG
from SVG import Plot
reload( SVG )
reload( Plot )
g = Plot.Plot({})
data1 = [ 1, 25, 2, 30, 3, 45 ]
g.add_data( { 'data': data1, 'title': 'foo' } )
g = Plot.Plot( {
'min_x_value': 0,
'min_y_value': 0,
'area_fill': True,
'stagger_x_labels': True,
'stagger_y_labels': True,
'show_x_guidelines': True
})
g.add_data( { 'data': [ 1, 25, 2, 30, 3, 45 ], 'title': 'foo' } )
g.add_data( { 'data': [ 1,30, 2, 31, 3, 40 ], 'title': 'foo2' } )
g.add_data( { 'data': [ .5,35, 1, 20, 3, 10.5 ], 'title': 'foo2' } )
res = g.burn()
f = open( r'c:\sample.svg', 'w' )
f.write( res )
f.close()
from SVG import TimeSeries
reload( TimeSeries )
g = TimeSeries.Plot( { } )
#g.timescale_divisions = '4 hours'
g.stagger_x_labels = True
g.add_data( { 'data': [ '2005-12-21T00:00:00', 20, '2005-12-22T00:00:00', 21 ], 'title': 'foo' } )
res = g.burn()
f = open( r'c:\timeseries.py.svg', 'w' )
f.write( res )
f.close()

32
testing.rb

@ -0,0 +1,32 @@
require 'SVG/Graph/Graph'
require 'SVG/Graph/Plot'
graph = SVG::Graph::Plot.new( {
:min_x_value=>0,
:min_y_value=>0,
:area_fill=> true,
:stagger_x_labels=>true,
:stagger_y_labels=>true
})
#data1 = [ 1,25, 2,30, 3,45 ]
graph.add_data( { :data=>[ 1,25, 2,30, 3,45 ], :title=>'foo' } )
graph.add_data( { :data=>[ 1,30, 2, 31, 3, 40 ], :title=>'foo2' } )
res = graph.burn()
f = File.new( 'c:\ruby.svg', 'w' )
f.write( res )
f.close()
require 'SVG/Graph/TimeSeries'
g = SVG::Graph::TimeSeries.new( { } )
g.add_data( { :data=> [ '2005-12-21T00:00:00', 20, '2005-12-22T00:00:00', 21 ], :title=> 'foo' } )
res = g.burn()
f = File.new( 'c:\timeseries.rb.svg', 'w' )
f.write( res )
f.close()
Loading…
Cancel
Save