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 ): 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"
while start < stop: while start < stop:
yield start yield float( start )
start += step start += step
class Plot( SVG.Graph ): class Plot( SVG.Graph ):
@ -145,12 +145,12 @@ class Plot( SVG.Graph ):
data['data'] = zip( *pairs ) data['data'] = zip( *pairs )
def calculate_left_margin( self ): 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 label_left = len( str( 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( 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 label_right = len( str( 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 )
@ -166,6 +166,7 @@ class Plot( SVG.Graph ):
min_value = min( min_value, spec_min ) min_value = min( min_value, spec_min )
range = max_value - min_value range = max_value - min_value
#side_pad = '%s_pad' % side #side_pad = '%s_pad' % side
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
@ -204,7 +205,6 @@ class Plot( SVG.Graph ):
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 )
for key,val in vars().items(): print key, val
return result return result
def field_width( self ): return self.field_size( 'x' ) def field_width( self ): return self.field_size( 'x' )
@ -223,11 +223,11 @@ class Plot( SVG.Graph ):
if self.area_fill: if self.area_fill:
graph_height = self.graph_height graph_height = self.graph_height
path = self._create_element( 'path', { 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() } ) 'class': 'fill%(line)d' % vars() } )
self.graph.appendChild( path ) self.graph.appendChild( path )
path = self._create_element( '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() } ) 'class': 'line%(line)d' % vars() } )
self.graph.appendChild( path ) self.graph.appendChild( path )
self.draw_data_points( line, data_points, graph_points ) 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 ) return map( self.transform_output_coordinates, data_points )
def get_lpath( self, 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 ) return 'L' + ' '.join( points )
def transform_output_coordinates( self, (x,y) ): def transform_output_coordinates( self, (x,y) ):

52
TimeSeries.py

@ -1,5 +1,12 @@
#!/usr/bin/env python #!/usr/bin/env python
import SVG 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 ): class Plot( SVG.Plot.Plot ):
"""=== For creating SVG plots of scalar temporal data """=== 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_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." __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 ): 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)
@ -114,13 +133,14 @@ class Plot( SVG.Plot.Plot ):
Note that the data must be in time,value pairs, and that the date format Note that the data must be in time,value pairs, and that the date format
may be any date that is parseable by ParseDate.""" 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 ): 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 # 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 = SVG.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 ):
@ -128,38 +148,34 @@ class Plot( SVG.Plot.Plot ):
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 fromtimestamp( x ).strftime( self.popup_format )
def get_x_labels( self ): 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 ): def get_x_values( self ):
result = self.get_x_timescale_division_values() result = self.get_x_timescale_division_values()
if result: return result if result: return result
return range( *self.x_range() ) return SVG.Plot.float_range( *self.x_range() )
def get_x_timescale_division_values( self ): def get_x_timescale_division_values( self ):
if not self.timescale_divisions: return if not self.timescale_divisions: return
min, max, scale_division = self.x_range() 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 ) 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 # copy amount and division_units into the local namespace
vars.update( m.groupdict() ) division_units = m.groupdict()['division_units'] or 'days'
division_units = division_units or 'days' amount = int( m.groupdict()['amount'] )
amount = int( amount )
if not amount: return if not amount: return
if division_units == 'weeks': delta = relativedelta( **{ division_units: amount } )
amount *= 7 result = self.get_time_range( min, max, delta )
division_units = 'days'
# strip off the plural (s)
division_units = division_units[:-1]
result = self.get_time_range( min, max, step, units )
return result 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 current = start
while current < stop: while current < stop:
yield current yield mktime( current.timetuple() )
current.replace( **{ units: current.getattr( units ) + step } ) current += delta
def parse_date( self, date_string ): 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 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 """Initialize the graph object with the graph settings. You won't
instantiate this class directly; see the subclass for options.""" instantiate this class directly; see the subclass for options."""
self.load_config( config )
self.clear_data() self.clear_data()
def load_config( self, config = None ): def load_config( self, config ):
if not config: config = self.defaults self.__dict__.update( config )
map( lambda pair: setattr( self, *pair ), config.items() )
def add_data( self, conf ): def add_data( self, conf ):
"""This method allows you do add data to the graph object. """This method allows you do add data to the graph object.
@ -314,7 +314,14 @@ Copyright 2005 Sandia National Laboratories
'class': 'dataPointLabel', 'class': 'dataPointLabel',
'style': '%(style)s stroke: #fff; stroke-width: 2;' % vars(), '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 ) self.foreground.appendChild( e )
def draw_x_labels( self ): def draw_x_labels( self ):
@ -345,7 +352,7 @@ Copyright 2005 Sandia National Laboratories
y += stagger y += stagger
graph_height = self.graph_height graph_height = self.graph_height
path = self._create_element( 'path', { 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' 'class': 'staggerGuideLine'
} ) } )
self.graph.appendChild( path ) self.graph.appendChild( path )
@ -391,7 +398,7 @@ Copyright 2005 Sandia National Laboratories
def get_y_offset( self ): def get_y_offset( self ):
#result = self.graph_height + self.y_label_offset( label_height ) #result = self.graph_height + self.y_label_offset( label_height )
result = self.graph_height + self.y_label_offset( self.field_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 return result
y_offset = property( get_y_offset ) y_offset = property( get_y_offset )
@ -405,11 +412,11 @@ Copyright 2005 Sandia National Laboratories
y = self.y_offset - ( label_height * index ) y = self.y_offset - ( label_height * index )
x = {True: 0, False:-3}[self.rotate_y_labels] 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 stagger = self.y_label_font_size + 5
x -= stagger x -= stagger
path = self._create_element( 'path', { 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' 'class': 'staggerGuideLine'
} ) } )
self.graph.appendChild( path ) self.graph.appendChild( path )
@ -424,10 +431,11 @@ Copyright 2005 Sandia National Laboratories
text.setAttribute( 'style', 'text-anchor: middle' ) text.setAttribute( 'style', 'text-anchor: middle' )
else: else:
text.setAttribute( 'y', str( y - self.y_label_font_size/2 ) ) 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 ): def draw_x_guidelines( self, label_height, count ):
"Draw the X-axis guidelines" "Draw the X-axis guidelines"
if not self.show_x_guidelines: return
# skip the first one # skip the first one
for count in range(1,count): for count in range(1,count):
start = label_height*count start = label_height*count
@ -440,11 +448,12 @@ Copyright 2005 Sandia National Laboratories
def draw_y_guidelines( self, label_height, count ): def draw_y_guidelines( self, label_height, count ):
"Draw the Y-axis guidelines" "Draw the Y-axis guidelines"
if not self.show_y_guidelines: return
for count in range( 1, count ): for count in range( 1, count ):
start = self.graph_height - label_height*count start = self.graph_height - label_height*count
stop = self.graph_width stop = self.graph_width
path = self._create_element( 'path', { path = self._create_element( 'path', {
'd': 'MO %(start)s h%(stop)s' % vars(), 'd': 'M 0 %(start)s h%(stop)s' % vars(),
'class': 'guideLines' } ) 'class': 'guideLines' } )
self.graph.appendChild( path ) self.graph.appendChild( path )
@ -582,7 +591,7 @@ Copyright 2005 Sandia National Laboratories
self.graph_height = self.height - self.border_top - self.border_bottom self.graph_height = self.height - self.border_top - self.border_bottom
def get_style( self ): def get_style( self ):
return """/* Copy from here for external style sheet */ result = """/* Copy from here for external style sheet */
.svgBackground{ .svgBackground{
fill:#ffffff; fill:#ffffff;
} }
@ -594,14 +603,14 @@ Copyright 2005 Sandia National Laboratories
.mainTitle{ .mainTitle{
text-anchor: middle; text-anchor: middle;
fill: #000000; fill: #000000;
font-size: #{title_font_size}px; font-size: %(title_font_size)dpx;
font-family: "Arial", sans-serif; font-family: "Arial", sans-serif;
font-weight: normal; font-weight: normal;
} }
.subTitle{ .subTitle{
text-anchor: middle; text-anchor: middle;
fill: #999999; fill: #999999;
font-size: #{subtitle_font_size}px; font-size: %(subtitle_font_size)dpx;
font-family: "Arial", sans-serif; font-family: "Arial", sans-serif;
font-weight: normal; font-weight: normal;
} }
@ -620,7 +629,7 @@ Copyright 2005 Sandia National Laboratories
.xAxisLabels{ .xAxisLabels{
text-anchor: middle; text-anchor: middle;
fill: #000000; fill: #000000;
font-size: #{x_label_font_size}px; font-size: %(x_label_font_size)dpx;
font-family: "Arial", sans-serif; font-family: "Arial", sans-serif;
font-weight: normal; font-weight: normal;
} }
@ -628,7 +637,7 @@ Copyright 2005 Sandia National Laboratories
.yAxisLabels{ .yAxisLabels{
text-anchor: end; text-anchor: end;
fill: #000000; fill: #000000;
font-size: #{y_label_font_size}px; font-size: %(y_label_font_size)dpx;
font-family: "Arial", sans-serif; font-family: "Arial", sans-serif;
font-weight: normal; font-weight: normal;
} }
@ -636,7 +645,7 @@ Copyright 2005 Sandia National Laboratories
.xAxisTitle{ .xAxisTitle{
text-anchor: middle; text-anchor: middle;
fill: #ff0000; fill: #ff0000;
font-size: #{x_title_font_size}px; font-size: %(x_title_font_size)dpx;
font-family: "Arial", sans-serif; font-family: "Arial", sans-serif;
font-weight: normal; font-weight: normal;
} }
@ -644,7 +653,7 @@ Copyright 2005 Sandia National Laboratories
.yAxisTitle{ .yAxisTitle{
fill: #ff0000; fill: #ff0000;
text-anchor: middle; text-anchor: middle;
font-size: #{y_title_font_size}px; font-size: %(y_title_font_size)dpx;
font-family: "Arial", sans-serif; font-family: "Arial", sans-serif;
font-weight: normal; font-weight: normal;
} }
@ -663,17 +672,19 @@ Copyright 2005 Sandia National Laboratories
stroke-width: 0.5px; stroke-width: 0.5px;
} }
%s %%s
.keyText{ .keyText{
fill: #000000; fill: #000000;
text-anchor:start; text-anchor:start;
font-size: #{key_font_size}px; font-size: %(key_font_size)dpx;
font-family: "Arial", sans-serif; font-family: "Arial", sans-serif;
font-weight: normal; font-weight: normal;
} }
/* End copy for external style sheet */ /* 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={} ): def _create_element( self, nodeName, attributes={} ):
"Create an XML node and set the attributes from a dict" "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() ) map( lambda a: node.setAttribute( *a ), attributes.items() )
return node 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 from SVG import Plot
reload( SVG ) reload( SVG )
reload( Plot ) reload( Plot )
g = Plot.Plot({}) g = Plot.Plot( {
data1 = [ 1, 25, 2, 30, 3, 45 ] 'min_x_value': 0,
g.add_data( { 'data': data1, 'title': 'foo' } ) '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() res = g.burn()
f = open( r'c:\sample.svg', 'w' ) f = open( r'c:\sample.svg', 'w' )
f.write( res ) f.write( res )
f.close() 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