mirror of https://github.com/Kozea/pygal.git
jaraco
17 years ago
6 changed files with 0 additions and 1539 deletions
@ -1,518 +0,0 @@ |
|||||||
#!/usr/bin/env python |
|
||||||
import SVG |
|
||||||
from itertools import izip, count, chain |
|
||||||
|
|
||||||
def get_pairs( i ): |
|
||||||
i = iter( i ) |
|
||||||
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 ): |
|
||||||
"""=== For creating SVG plots of scalar data |
|
||||||
|
|
||||||
= Synopsis |
|
||||||
|
|
||||||
require 'SVG/Graph/Plot' |
|
||||||
|
|
||||||
# Data sets are x,y pairs |
|
||||||
# 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 |
|
||||||
# by the plot along the X-axis. |
|
||||||
projection = [ |
|
||||||
6, 11, 0, 5, 18, 7, 1, 11, 13, 9, 1, 2, 19, 0, 3, 13, |
|
||||||
7, 9 |
|
||||||
] |
|
||||||
actual = [ |
|
||||||
0, 18, 8, 15, 9, 4, 18, 14, 10, 2, 11, 6, 14, 12, |
|
||||||
15, 6, 4, 17, 2, 12 |
|
||||||
] |
|
||||||
|
|
||||||
graph = SVG::Graph::Plot.new({ |
|
||||||
:height => 500, |
|
||||||
:width => 300, |
|
||||||
:key => true, |
|
||||||
:scale_x_integers => true, |
|
||||||
:scale_y_integerrs => true, |
|
||||||
}) |
|
||||||
|
|
||||||
graph.add_data({ |
|
||||||
:data => projection |
|
||||||
:title => 'Projected', |
|
||||||
}) |
|
||||||
|
|
||||||
graph.add_data({ |
|
||||||
:data => actual, |
|
||||||
:title => 'Actual', |
|
||||||
}) |
|
||||||
|
|
||||||
print graph.burn() |
|
||||||
|
|
||||||
= Description |
|
||||||
|
|
||||||
Produces a graph of scalar data. |
|
||||||
|
|
||||||
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 |
|
||||||
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 |
|
||||||
generated - with or without a key, data elements at each point, title, |
|
||||||
subtitle etc. |
|
||||||
|
|
||||||
= Examples |
|
||||||
|
|
||||||
http://www.germane-software/repositories/public/SVG/test/plot.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. |
|
||||||
|
|
||||||
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, 5,6] # A data set with 2 points: (1,2) and (5,6) |
|
||||||
|
|
||||||
= See also |
|
||||||
|
|
||||||
* SVG::Graph::Graph |
|
||||||
* SVG::Graph::BarHorizontal |
|
||||||
* SVG::Graph::Bar |
|
||||||
* SVG::Graph::Line |
|
||||||
* SVG::Graph::Pie |
|
||||||
* SVG::Graph::TimeSeries |
|
||||||
|
|
||||||
== Author |
|
||||||
|
|
||||||
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> |
|
||||||
|
|
||||||
Copyright 2004 Sean E. Russell |
|
||||||
This software is available under the Ruby license[LICENSE.txt]""" |
|
||||||
|
|
||||||
top_align = right_align = top_font = right_font = 1 |
|
||||||
|
|
||||||
|
|
||||||
"""Determines the scaling for the Y axis divisions. |
|
||||||
|
|
||||||
graph.scale_y_divisions = 0.5 |
|
||||||
|
|
||||||
would cause the graph to attempt to generate labels stepped by 0.5; EG: |
|
||||||
0, 0.5, 1, 1.5, 2, ...""" |
|
||||||
scale_y_divisions = None |
|
||||||
"Make the X axis labels integers" |
|
||||||
scale_x_integers = False |
|
||||||
"Make the Y axis labels integers" |
|
||||||
scale_y_integers = False |
|
||||||
"Fill the area under the line" |
|
||||||
area_fill = False |
|
||||||
"""Show a small circle on the graph where the line |
|
||||||
goes from one point to the next.""" |
|
||||||
show_data_points = True |
|
||||||
"Indicate whether the lines should be drawn between points" |
|
||||||
draw_lines_between_points = True |
|
||||||
"Set the minimum value of the X axis" |
|
||||||
min_x_value = None |
|
||||||
"Set the minimum value of the Y axis" |
|
||||||
min_y_value = None |
|
||||||
"Set the maximum value of the X axis" |
|
||||||
max_x_value = None |
|
||||||
"Set the maximum value of the Y axis" |
|
||||||
max_y_value = None |
|
||||||
|
|
||||||
stacked = False |
|
||||||
|
|
||||||
@apply |
|
||||||
def scale_x_divisions(): |
|
||||||
doc = """Determines the scaling for the X axis divisions. |
|
||||||
|
|
||||||
graph.scale_x_divisions = 2 |
|
||||||
|
|
||||||
would cause the graph to attempt to generate labels stepped by 2; EG: |
|
||||||
0,2,4,6,8...""" |
|
||||||
def fget( self ): |
|
||||||
return getattr( self, '_scale_x_divisions', None ) |
|
||||||
def fset( self, val ): |
|
||||||
self._scale_x_divisions = val |
|
||||||
return property(**locals()) |
|
||||||
|
|
||||||
def validate_data( self, data ): |
|
||||||
if len( data['data'] ) % 2 != 0: |
|
||||||
raise "Expecting x,y pairs for data points for %s." % self.__class__.__name__ |
|
||||||
|
|
||||||
def process_data( self, data ): |
|
||||||
pairs = list( get_pairs( data['data'] ) ) |
|
||||||
pairs.sort() |
|
||||||
data['data'] = zip( *pairs ) |
|
||||||
|
|
||||||
def calculate_left_margin( self ): |
|
||||||
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( 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 ) |
|
||||||
|
|
||||||
def data_max( self, axis ): |
|
||||||
data_index = getattr( self, '%s_data_index' % axis ) |
|
||||||
max_value = max( chain( *map( lambda set: set['data'][data_index], self.data ) ) ) |
|
||||||
# above is same as |
|
||||||
#max_value = max( map( lambda set: max( set['data'][data_index] ), self.data ) ) |
|
||||||
spec_max = getattr( self, 'max_%s_value' % axis ) |
|
||||||
max_value = max( max_value, spec_max ) |
|
||||||
return max_value |
|
||||||
|
|
||||||
def data_min( self, axis ): |
|
||||||
data_index = getattr( self, '%s_data_index' % axis ) |
|
||||||
min_value = min( chain( *map( lambda set: set['data'][data_index], self.data ) ) ) |
|
||||||
spec_min = getattr( self, 'min_%s_value' % axis ) |
|
||||||
if spec_min is not None: |
|
||||||
min_value = min( min_value, spec_min ) |
|
||||||
return min_value |
|
||||||
|
|
||||||
x_data_index = 0 |
|
||||||
y_data_index = 1 |
|
||||||
def data_range( self, axis ): |
|
||||||
side = { 'x': 'right', 'y': 'top' }[axis] |
|
||||||
|
|
||||||
min_value = self.data_min( axis ) |
|
||||||
max_value = self.data_max( axis ) |
|
||||||
range = max_value - min_value |
|
||||||
|
|
||||||
side_pad = range / 20.0 or 10 |
|
||||||
scale_range = ( max_value + side_pad ) - min_value |
|
||||||
|
|
||||||
scale_division = getattr( self, 'scale_%s_divisions' % axis ) or ( scale_range / 10.0 ) |
|
||||||
|
|
||||||
if getattr( self, 'scale_%s_integers' % axis ): |
|
||||||
scale_division = scale_division.round() or 1 |
|
||||||
|
|
||||||
return min_value, max_value, scale_division |
|
||||||
|
|
||||||
def x_range( self ): return self.data_range( 'x' ) |
|
||||||
def y_range( self ): return self.data_range( 'y' ) |
|
||||||
|
|
||||||
def get_data_values( self, axis ): |
|
||||||
min_value, max_value, scale_division = self.data_range( axis ) |
|
||||||
return tuple( float_range( *self.data_range( axis ) ) ) |
|
||||||
|
|
||||||
def get_x_values( self ): return self.get_data_values( 'x' ) |
|
||||||
def get_y_values( self ): return self.get_data_values( 'y' ) |
|
||||||
|
|
||||||
def get_x_labels( self ): |
|
||||||
return map( str, self.get_x_values() ) |
|
||||||
def get_y_labels( self ): |
|
||||||
return map( str, self.get_y_values() ) |
|
||||||
|
|
||||||
def field_size( self, axis ): |
|
||||||
size = { 'x': 'width', 'y': 'height' }[axis] |
|
||||||
side = { 'x': 'right', 'y': 'top' }[axis] |
|
||||||
values = getattr( self, 'get_%s_values' % axis )() |
|
||||||
max_d = self.data_max( axis ) |
|
||||||
dx = float( max_d - values[-1] ) / ( values[-1] - values[-2] ) |
|
||||||
graph_size = getattr( self, 'graph_%s' % size ) |
|
||||||
side_font = getattr( self, '%s_font' % side ) |
|
||||||
side_align = getattr( self, '%s_align' % side ) |
|
||||||
result = ( float( graph_size ) - self.font_size*2*side_font ) / \ |
|
||||||
( len( values ) + dx - side_align ) |
|
||||||
return result |
|
||||||
|
|
||||||
def field_width( self ): return self.field_size( 'x' ) |
|
||||||
def field_height( self ): return self.field_size( 'y' ) |
|
||||||
|
|
||||||
def draw_data( self ): |
|
||||||
self.load_transform_parameters() |
|
||||||
for line, data in izip( count(1), self.data ): |
|
||||||
x_start, y_start = self.transform_output_coordinates( |
|
||||||
( data['data'][self.x_data_index][0], |
|
||||||
data['data'][self.y_data_index][0] ) |
|
||||||
) |
|
||||||
data_points = zip( *data['data'] ) |
|
||||||
graph_points = self.get_graph_points( data_points ) |
|
||||||
lpath = self.get_lpath( graph_points ) |
|
||||||
if self.area_fill: |
|
||||||
graph_height = self.graph_height |
|
||||||
path = self._create_element( 'path', { |
|
||||||
'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 ) |
|
||||||
if self.draw_lines_between_points: |
|
||||||
path = self._create_element( 'path', { |
|
||||||
'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 ) |
|
||||||
self._draw_constant_lines( ) |
|
||||||
del self.__transform_parameters |
|
||||||
|
|
||||||
def add_constant_line( self, value, label = None, style = None ): |
|
||||||
self.constant_lines = getattr( self, 'constant_lines', [] ) |
|
||||||
self.constant_lines.append( ( value, label, style ) ) |
|
||||||
|
|
||||||
def _draw_constant_lines( self ): |
|
||||||
if hasattr( self, 'constant_lines' ): |
|
||||||
map( self.__draw_constant_line, self.constant_lines ) |
|
||||||
|
|
||||||
def __draw_constant_line( self, ( value, label, style ) ): |
|
||||||
"Draw a constant line on the y-axis with the label" |
|
||||||
start = self.transform_output_coordinates( ( 0, value ) )[1] |
|
||||||
stop = self.graph_width |
|
||||||
path = self._create_element( 'path', { |
|
||||||
'd': 'M 0 %(start)s h%(stop)s' % vars(), |
|
||||||
'class': 'constantLine' } ) |
|
||||||
if style: |
|
||||||
path['style'] = style |
|
||||||
self.graph.appendChild( path ) |
|
||||||
text = self._create_element( 'text', { |
|
||||||
'x': str( 2 ), |
|
||||||
'y': str( start - 2 ), |
|
||||||
'class': 'constantLine' } ) |
|
||||||
text.appendChild( self._doc.createTextNode( label ) ) |
|
||||||
self.graph.appendChild( text ) |
|
||||||
|
|
||||||
def load_transform_parameters( self ): |
|
||||||
"Cache the parameters necessary to transform x & y coordinates" |
|
||||||
x_min, x_max, x_div = self.x_range() |
|
||||||
y_min, y_max, y_div = self.y_range() |
|
||||||
x_step = ( float( self.graph_width ) - self.font_size*2 ) / \ |
|
||||||
( x_max - x_min ) |
|
||||||
y_step = ( float( self.graph_height ) - self.font_size*2 ) / \ |
|
||||||
( y_max - y_min ) |
|
||||||
self.__transform_parameters = dict( vars() ) |
|
||||||
del self.__transform_parameters['self'] |
|
||||||
|
|
||||||
def get_graph_points( self, data_points ): |
|
||||||
return map( self.transform_output_coordinates, data_points ) |
|
||||||
|
|
||||||
def get_lpath( self, points ): |
|
||||||
points = map( lambda p: "%f %f" % p, points ) |
|
||||||
return 'L' + ' '.join( points ) |
|
||||||
|
|
||||||
def transform_output_coordinates( self, (x,y) ): |
|
||||||
x_min = self.__transform_parameters['x_min'] |
|
||||||
x_step = self.__transform_parameters['x_step'] |
|
||||||
y_min = self.__transform_parameters['y_min'] |
|
||||||
y_step = self.__transform_parameters['y_step'] |
|
||||||
#locals().update( self.__transform_parameters ) |
|
||||||
#vars().update( self.__transform_parameters ) |
|
||||||
x = ( x - x_min ) * x_step |
|
||||||
y = self.graph_height - ( y - y_min ) * y_step |
|
||||||
return x,y |
|
||||||
|
|
||||||
def draw_data_points( self, line, data_points, graph_points ): |
|
||||||
if not self.show_data_points \ |
|
||||||
and not self.show_data_values: return |
|
||||||
for ((dx,dy),(gx,gy)) in izip( data_points, graph_points ): |
|
||||||
if self.show_data_points: |
|
||||||
circle = self._create_element( 'circle', { |
|
||||||
'cx': str( gx ), |
|
||||||
'cy': str( gy ), |
|
||||||
'r': '2.5', |
|
||||||
'class': 'dataPoint%(line)s' % vars() } ) |
|
||||||
self.graph.appendChild( circle ) |
|
||||||
if self.show_data_values: |
|
||||||
self.add_popup( gx, gy, self.format( dx, dy ) ) |
|
||||||
self.make_datapoint_text( gx, gy-6, dy ) |
|
||||||
|
|
||||||
def format( self, x, y ): |
|
||||||
return '(%0.2f, %0.2f)' % (x,y) |
|
||||||
|
|
||||||
def get_css( self ): |
|
||||||
return """/* default line styles */ |
|
||||||
.line1{ |
|
||||||
fill: none; |
|
||||||
stroke: #ff0000; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.line2{ |
|
||||||
fill: none; |
|
||||||
stroke: #0000ff; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.line3{ |
|
||||||
fill: none; |
|
||||||
stroke: #00ff00; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.line4{ |
|
||||||
fill: none; |
|
||||||
stroke: #ffcc00; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.line5{ |
|
||||||
fill: none; |
|
||||||
stroke: #00ccff; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.line6{ |
|
||||||
fill: none; |
|
||||||
stroke: #ff00ff; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.line7{ |
|
||||||
fill: none; |
|
||||||
stroke: #00ffff; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.line8{ |
|
||||||
fill: none; |
|
||||||
stroke: #ffff00; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.line9{ |
|
||||||
fill: none; |
|
||||||
stroke: #ccc6666; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.line10{ |
|
||||||
fill: none; |
|
||||||
stroke: #663399; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.line11{ |
|
||||||
fill: none; |
|
||||||
stroke: #339900; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.line12{ |
|
||||||
fill: none; |
|
||||||
stroke: #9966FF; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
/* default fill styles */ |
|
||||||
.fill1{ |
|
||||||
fill: #cc0000; |
|
||||||
fill-opacity: 0.2; |
|
||||||
stroke: none; |
|
||||||
} |
|
||||||
.fill2{ |
|
||||||
fill: #0000cc; |
|
||||||
fill-opacity: 0.2; |
|
||||||
stroke: none; |
|
||||||
} |
|
||||||
.fill3{ |
|
||||||
fill: #00cc00; |
|
||||||
fill-opacity: 0.2; |
|
||||||
stroke: none; |
|
||||||
} |
|
||||||
.fill4{ |
|
||||||
fill: #ffcc00; |
|
||||||
fill-opacity: 0.2; |
|
||||||
stroke: none; |
|
||||||
} |
|
||||||
.fill5{ |
|
||||||
fill: #00ccff; |
|
||||||
fill-opacity: 0.2; |
|
||||||
stroke: none; |
|
||||||
} |
|
||||||
.fill6{ |
|
||||||
fill: #ff00ff; |
|
||||||
fill-opacity: 0.2; |
|
||||||
stroke: none; |
|
||||||
} |
|
||||||
.fill7{ |
|
||||||
fill: #00ffff; |
|
||||||
fill-opacity: 0.2; |
|
||||||
stroke: none; |
|
||||||
} |
|
||||||
.fill8{ |
|
||||||
fill: #ffff00; |
|
||||||
fill-opacity: 0.2; |
|
||||||
stroke: none; |
|
||||||
} |
|
||||||
.fill9{ |
|
||||||
fill: #cc6666; |
|
||||||
fill-opacity: 0.2; |
|
||||||
stroke: none; |
|
||||||
} |
|
||||||
.fill10{ |
|
||||||
fill: #663399; |
|
||||||
fill-opacity: 0.2; |
|
||||||
stroke: none; |
|
||||||
} |
|
||||||
.fill11{ |
|
||||||
fill: #339900; |
|
||||||
fill-opacity: 0.2; |
|
||||||
stroke: none; |
|
||||||
} |
|
||||||
.fill12{ |
|
||||||
fill: #9966FF; |
|
||||||
fill-opacity: 0.2; |
|
||||||
stroke: none; |
|
||||||
} |
|
||||||
/* default line styles */ |
|
||||||
.key1,.dataPoint1{ |
|
||||||
fill: #ff0000; |
|
||||||
stroke: none; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.key2,.dataPoint2{ |
|
||||||
fill: #0000ff; |
|
||||||
stroke: none; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.key3,.dataPoint3{ |
|
||||||
fill: #00ff00; |
|
||||||
stroke: none; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.key4,.dataPoint4{ |
|
||||||
fill: #ffcc00; |
|
||||||
stroke: none; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.key5,.dataPoint5{ |
|
||||||
fill: #00ccff; |
|
||||||
stroke: none; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.key6,.dataPoint6{ |
|
||||||
fill: #ff00ff; |
|
||||||
stroke: none; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.key7,.dataPoint7{ |
|
||||||
fill: #00ffff; |
|
||||||
stroke: none; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.key8,.dataPoint8{ |
|
||||||
fill: #ffff00; |
|
||||||
stroke: none; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.key9,.dataPoint9{ |
|
||||||
fill: #cc6666; |
|
||||||
stroke: none; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.key10,.dataPoint10{ |
|
||||||
fill: #663399; |
|
||||||
stroke: none; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.key11,.dataPoint11{ |
|
||||||
fill: #339900; |
|
||||||
stroke: none; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.key12,.dataPoint12{ |
|
||||||
fill: #9966FF; |
|
||||||
stroke: none; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
.constantLine{ |
|
||||||
color: navy; |
|
||||||
stroke: navy; |
|
||||||
stroke-width: 1px; |
|
||||||
stroke-dasharray: 9 1 1; |
|
||||||
} |
|
||||||
""" |
|
@ -1,181 +0,0 @@ |
|||||||
#!/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 |
|
||||||
|
|
||||||
= Synopsis |
|
||||||
|
|
||||||
import SVG.TimeSeries |
|
||||||
|
|
||||||
# Data sets are x,y pairs |
|
||||||
data1 = ["6/17/72", 11, "1/11/72", 7, "4/13/04 17:31", 11, |
|
||||||
"9/11/01", 9, "9/1/85", 2, "9/1/88", 1, "1/15/95", 13] |
|
||||||
data2 = ["8/1/73", 18, "3/1/77", 15, "10/1/98", 4, |
|
||||||
"5/1/02", 14, "3/1/95", 6, "8/1/91", 12, "12/1/87", 6, |
|
||||||
"5/1/84", 17, "10/1/80", 12] |
|
||||||
|
|
||||||
graph = SVG::Graph::TimeSeries.new( { |
|
||||||
:width => 640, |
|
||||||
:height => 480, |
|
||||||
:graph_title => title, |
|
||||||
:show_graph_title => true, |
|
||||||
:no_css => true, |
|
||||||
:key => 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", |
|
||||||
:show_y_title => true, |
|
||||||
:y_title => "Ice Cream Cones", |
|
||||||
:y_title_text_direction => :bt, |
|
||||||
:stagger_x_labels => true, |
|
||||||
:x_label_format => "%m/%d/%y", |
|
||||||
}) |
|
||||||
|
|
||||||
graph.add_data({ |
|
||||||
:data => projection |
|
||||||
:title => 'Projected', |
|
||||||
}) |
|
||||||
|
|
||||||
graph.add_data({ |
|
||||||
:data => actual, |
|
||||||
:title => 'Actual', |
|
||||||
}) |
|
||||||
|
|
||||||
print graph.burn() |
|
||||||
|
|
||||||
= Description |
|
||||||
|
|
||||||
Produces a graph of temporal scalar data. |
|
||||||
|
|
||||||
= Examples |
|
||||||
|
|
||||||
http://www.germane-software/repositories/public/SVG/test/timeseries.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. |
|
||||||
|
|
||||||
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) |
|
||||||
[ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and |
|
||||||
# ("14:20",6) |
|
||||||
|
|
||||||
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 |
|
||||||
|
|
||||||
== Author |
|
||||||
|
|
||||||
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> |
|
||||||
|
|
||||||
Copyright 2004 Sean E. Russell |
|
||||||
This software is available under the Ruby license[LICENSE.txt] |
|
||||||
""" |
|
||||||
popup_format = x_label_format = '%Y-%m-%d %H:%M:%S' |
|
||||||
__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) |
|
||||||
d2 = [ "01:00",2, "14:20",6] # A data set with 2 points: ("01:00",2) and |
|
||||||
# ("14:20",6) |
|
||||||
graph.add_data( |
|
||||||
:data => d1, |
|
||||||
:title => 'One' |
|
||||||
) |
|
||||||
graph.add_data( |
|
||||||
:data => d2, |
|
||||||
:title => 'Two' |
|
||||||
) |
|
||||||
|
|
||||||
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( Plot, self ).add_data( data ) |
|
||||||
|
|
||||||
def process_data( self, 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 ): |
|
||||||
self._min_x_value = self.parse_date( date ) |
|
||||||
min_x_value = property( get_min_x_value, set_min_x_value ) |
|
||||||
|
|
||||||
def format( self, x, y ): |
|
||||||
return fromtimestamp( x ).strftime( self.popup_format ) |
|
||||||
|
|
||||||
def get_x_labels( self ): |
|
||||||
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 tuple( 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 |
|
||||||
division_units = m.groupdict()['division_units'] or 'days' |
|
||||||
amount = int( m.groupdict()['amount'] ) |
|
||||||
if not amount: return |
|
||||||
delta = relativedelta( **{ division_units: amount } ) |
|
||||||
result = tuple( self.get_time_range( min, max, delta ) ) |
|
||||||
return result |
|
||||||
|
|
||||||
def get_time_range( self, start, stop, delta ): |
|
||||||
start, stop = map( fromtimestamp, (start, stop ) ) |
|
||||||
current = start |
|
||||||
while current <= stop: |
|
||||||
yield mktime( current.timetuple() ) |
|
||||||
current += delta |
|
||||||
|
|
||||||
def parse_date( self, date_string ): |
|
||||||
return mktime( parse( date_string ).timetuple() ) |
|
@ -1,728 +0,0 @@ |
|||||||
# -*- coding: UTF-8 -*- |
|
||||||
|
|
||||||
__all__ = ( 'Plot', 'TimeSeries' ) |
|
||||||
|
|
||||||
from xml.dom import minidom as dom |
|
||||||
from operator import itemgetter |
|
||||||
from itertools import islice |
|
||||||
|
|
||||||
try: |
|
||||||
import zlib |
|
||||||
__have_zlib = True |
|
||||||
except ImportError: |
|
||||||
__have_zlib = False |
|
||||||
|
|
||||||
def sort_multiple( arrays ): |
|
||||||
"sort multiple lists (of equal size) using the first list for the sort keys" |
|
||||||
tuples = zip( *arrays ) |
|
||||||
tuples.sort() |
|
||||||
return zip( *tuples ) |
|
||||||
|
|
||||||
class Graph( object ): |
|
||||||
"""=== Base object for generating SVG Graphs |
|
||||||
|
|
||||||
== Synopsis |
|
||||||
|
|
||||||
This class is only used as a superclass of specialized charts. Do not |
|
||||||
attempt to use this class directly, unless creating a new chart type. |
|
||||||
|
|
||||||
For examples of how to subclass this class, see the existing specific |
|
||||||
subclasses, such as SVG.Pie. |
|
||||||
|
|
||||||
== Examples |
|
||||||
|
|
||||||
For examples of how to use this package, see either the test files, or |
|
||||||
the documentation for the specific class you want to use. |
|
||||||
|
|
||||||
* file:test/plot.rb |
|
||||||
* file:test/single.rb |
|
||||||
* file:test/test.rb |
|
||||||
* file:test/timeseries.rb |
|
||||||
|
|
||||||
== Description |
|
||||||
|
|
||||||
This package should be used as a base for creating SVG graphs. |
|
||||||
|
|
||||||
== Acknowledgements |
|
||||||
|
|
||||||
Sean E. Russel for creating the SVG::Graph Ruby package from which this |
|
||||||
Python port is derived. |
|
||||||
|
|
||||||
Leo Lapworth for creating the SVG::TT::Graph package which the Ruby |
|
||||||
port is based on. |
|
||||||
|
|
||||||
Stephen Morgan for creating the TT template and SVG. |
|
||||||
|
|
||||||
== See |
|
||||||
|
|
||||||
* SVG.BarHorizontal |
|
||||||
* SVG.Bar |
|
||||||
* SVG.Line |
|
||||||
* SVG.Pie |
|
||||||
* SVG.Plot |
|
||||||
* SVG.TimeSeries |
|
||||||
|
|
||||||
== Author |
|
||||||
|
|
||||||
Jason R. Coombs <jaraco@jaraco.com> |
|
||||||
|
|
||||||
Copyright 2005 Sandia National Laboratories |
|
||||||
""" |
|
||||||
width= 500 |
|
||||||
height= 300 |
|
||||||
show_x_guidelines= False |
|
||||||
show_y_guidelines= True |
|
||||||
show_data_values= True |
|
||||||
min_scale_value= 0 |
|
||||||
show_x_labels= True |
|
||||||
stagger_x_labels= False |
|
||||||
rotate_x_labels= False |
|
||||||
step_x_labels= 1 |
|
||||||
step_include_first_x_label= True |
|
||||||
show_y_labels= True |
|
||||||
rotate_y_labels= False |
|
||||||
stagger_y_labels= False |
|
||||||
step_include_first_y_label= True |
|
||||||
step_y_labels= 1 |
|
||||||
scale_integers= False |
|
||||||
show_x_title= False |
|
||||||
x_title= 'X Field names' |
|
||||||
show_y_title= False |
|
||||||
y_title_text_direction= 'bt' # 'bt' for bottom to top; 'tb' for top to bottom |
|
||||||
y_title= 'Y Scale' |
|
||||||
show_graph_title= False |
|
||||||
graph_title= 'Graph Title' |
|
||||||
show_graph_subtitle= False |
|
||||||
graph_subtitle= 'Graph Subtitle' |
|
||||||
key= True |
|
||||||
key_position= 'right' # 'bottom' or 'right', |
|
||||||
|
|
||||||
font_size= 12 |
|
||||||
title_font_size= 16 |
|
||||||
subtitle_font_size= 14 |
|
||||||
x_label_font_size= 12 |
|
||||||
x_title_font_size= 14 |
|
||||||
y_label_font_size= 12 |
|
||||||
y_title_font_size= 14 |
|
||||||
key_font_size= 10 |
|
||||||
|
|
||||||
no_css= False |
|
||||||
add_popups= False |
|
||||||
|
|
||||||
top_align = top_font = right_align = right_font = 0 |
|
||||||
|
|
||||||
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 ): |
|
||||||
self.__dict__.update( config ) |
|
||||||
|
|
||||||
def add_data( self, conf ): |
|
||||||
"""This method allows you do add data to the graph object. |
|
||||||
It can be called several times to add more data sets in. |
|
||||||
|
|
||||||
>>> data_sales_02 = [12, 45, 21] |
|
||||||
>>> graph.add_data({ |
|
||||||
... 'data': data_sales_02, |
|
||||||
... 'title': 'Sales 2002' |
|
||||||
... }) |
|
||||||
""" |
|
||||||
self.validate_data( conf ) |
|
||||||
self.process_data( conf ) |
|
||||||
self.data.append( conf ) |
|
||||||
|
|
||||||
def validate_data( self, data ): |
|
||||||
try: |
|
||||||
assert( isinstance( conf['data'], ( tuple, list ) ) ) |
|
||||||
except TypeError, e: |
|
||||||
raise TypeError, "conf should be dictionary with 'data' and other items" |
|
||||||
except AssertionError: |
|
||||||
if not hasattr( conf['data'], '__iter__' ): |
|
||||||
raise TypeError, "conf['data'] should be tuple or list or iterable" |
|
||||||
|
|
||||||
def process_data( data ): pass |
|
||||||
|
|
||||||
def clear_data( self ): |
|
||||||
"""This method removes all data from the object so that you can |
|
||||||
reuse it to create a new graph but with the same config options. |
|
||||||
|
|
||||||
>>> graph.clear_data()""" |
|
||||||
self.data = [] |
|
||||||
|
|
||||||
def burn( self ): |
|
||||||
"""This method processes the template with the data and |
|
||||||
config which has been set and returns the resulting SVG. |
|
||||||
|
|
||||||
This method will croak unless at least one data set has |
|
||||||
been added to the graph object. |
|
||||||
|
|
||||||
Ex: graph.burn()""" |
|
||||||
if not self.data: raise ValueError( "No data available" ) |
|
||||||
|
|
||||||
if hasattr( self, 'calculations' ): self.calculations() |
|
||||||
|
|
||||||
self.start_svg() |
|
||||||
self.calculate_graph_dimensions() |
|
||||||
self.foreground = self._create_element( "g" ) |
|
||||||
self.draw_graph() |
|
||||||
self.draw_titles() |
|
||||||
self.draw_legend() |
|
||||||
self.draw_data() |
|
||||||
self.graph.appendChild( self.foreground ) |
|
||||||
self.style() |
|
||||||
|
|
||||||
data = self._doc.toprettyxml() |
|
||||||
|
|
||||||
if hasattr( self, 'compress' ) and self.compress: |
|
||||||
if __have_zlib: |
|
||||||
data = zlib.compress( data ) |
|
||||||
else: |
|
||||||
data += '<!-- Python zlib not available for SVGZ -->' |
|
||||||
|
|
||||||
return data |
|
||||||
|
|
||||||
KEY_BOX_SIZE = 12 |
|
||||||
|
|
||||||
def calculate_left_margin( self ): |
|
||||||
"""Override this (and call super) to change the margin to the left |
|
||||||
of the plot area. Results in border_left being set.""" |
|
||||||
bl = 7 |
|
||||||
# Check for Y labels |
|
||||||
if self.rotate_y_labels: |
|
||||||
max_y_label_height_px = self.y_label_font_size |
|
||||||
else: |
|
||||||
label_lengths = map( len, self.get_y_labels() ) |
|
||||||
max_y_label_len = max( label_lengths ) |
|
||||||
max_y_label_height_px = 0.6 * max_y_label_len * self.y_label_font_size |
|
||||||
if self.show_y_labels: bl += max_y_label_height_px |
|
||||||
if self.stagger_y_labels: bl += max_y_label_height_px + 10 |
|
||||||
if self.show_y_title: bl += self.y_title_font_size + 5 |
|
||||||
self.border_left = bl |
|
||||||
|
|
||||||
def max_y_label_width_px( self ): |
|
||||||
"""Calculates the width of the widest Y label. This will be the |
|
||||||
character height if the Y labels are rotated.""" |
|
||||||
if self.rotate_y_labels: |
|
||||||
return self.font_size |
|
||||||
|
|
||||||
def calculate_right_margin( self ): |
|
||||||
"""Override this (and call super) to change the margin to the right |
|
||||||
of the plot area. Results in border_right being set.""" |
|
||||||
br = 7 |
|
||||||
if self.key and self.key_position == 'right': |
|
||||||
max_key_len = max( map( len, self.keys() ) ) |
|
||||||
br += max_key_len * self.key_font_size * 0.6 |
|
||||||
br += self.KEY_BOX_SIZE |
|
||||||
br += 10 # Some padding around the box |
|
||||||
self.border_right = br |
|
||||||
|
|
||||||
def calculate_top_margin( self ): |
|
||||||
"""Override this (and call super) to change the margin to the top |
|
||||||
of the plot area. Results in border_top being set.""" |
|
||||||
self.border_top = 5 |
|
||||||
if self.show_graph_title: self.border_top += self.title_font_size |
|
||||||
self.border_top += 5 |
|
||||||
if self.show_graph_subtitle: self.border_top += self.subtitle_font_size |
|
||||||
|
|
||||||
def add_popup( self, x, y, label ): |
|
||||||
"Adds pop-up point information to a graph." |
|
||||||
txt_width = len( label ) * self.font_size * 0.6 + 10 |
|
||||||
tx = x + [5,-5][int(x+txt_width > self.width)] |
|
||||||
t = self._create_element( 'text' ) |
|
||||||
anchor = ['start', 'end'][x+txt_width > self.width] |
|
||||||
style = 'fill: #000; text-anchor: %s;' % anchor |
|
||||||
id = 'label-%s' % label |
|
||||||
attributes = { 'x': str( tx ), |
|
||||||
'y': str( y - self.font_size ), |
|
||||||
'visibility': 'hidden', |
|
||||||
'style': style, |
|
||||||
'text': label, |
|
||||||
'id': id |
|
||||||
} |
|
||||||
map( lambda a: t.setAttribute( *a ), attributes.items() ) |
|
||||||
self.foreground.appendChild( t ) |
|
||||||
|
|
||||||
visibility = "document.getElementById(%s).setAttribute('visibility', %%s )" % id |
|
||||||
t = self._create_element( 'circle' ) |
|
||||||
attributes = { 'cx': str( x ), |
|
||||||
'cy': str( y ), |
|
||||||
'r': 10, |
|
||||||
'style': 'opacity: 0;', |
|
||||||
'onmouseover': visibility % 'visible', |
|
||||||
'onmouseout': visibility % 'hidden', |
|
||||||
} |
|
||||||
map( lambda a: t.setAttribute( *a ), attributes.items() ) |
|
||||||
|
|
||||||
def calculate_bottom_margin( self ): |
|
||||||
"""Override this (and call super) to change the margin to the bottom |
|
||||||
of the plot area. Results in border_bottom being set.""" |
|
||||||
bb = 7 |
|
||||||
if self.key and self.key_position == 'bottom': |
|
||||||
bb += len( self.data ) * ( self.font_size + 5 ) |
|
||||||
bb += 10 |
|
||||||
if self.show_x_labels: |
|
||||||
max_x_label_height_px = self.x_label_font_size |
|
||||||
if self.rotate_x_labels: |
|
||||||
label_lengths = map( len, self.get_x_labels() ) |
|
||||||
max_x_label_len = reduce( max, label_lengths ) |
|
||||||
max_x_label_height_px *= 0.6 * max_x_label_len |
|
||||||
bb += max_x_label_height_px |
|
||||||
if self.stagger_x_labels: bb += max_x_label_height_px + 10 |
|
||||||
if self.show_x_title: bb += self.x_title_font_size + 5 |
|
||||||
self.border_bottom = bb |
|
||||||
|
|
||||||
def draw_graph( self ): |
|
||||||
transform = 'translate ( %s %s )' % ( self.border_left, self.border_top ) |
|
||||||
self.graph = self._create_element( 'g', { 'transform': transform } ) |
|
||||||
self.root.appendChild( self.graph ) |
|
||||||
|
|
||||||
self.graph.appendChild( self._create_element( 'rect', { |
|
||||||
'x': '0', |
|
||||||
'y': '0', |
|
||||||
'width': str( self.graph_width ), |
|
||||||
'height': str( self.graph_height ), |
|
||||||
'class': 'graphBackground' |
|
||||||
} ) ) |
|
||||||
|
|
||||||
#Axis |
|
||||||
self.graph.appendChild( self._create_element( 'path', { |
|
||||||
'd': 'M 0 0 v%s' % self.graph_height, |
|
||||||
'class': 'axis', |
|
||||||
'id': 'xAxis' |
|
||||||
} ) ) |
|
||||||
self.graph.appendChild( self._create_element( 'path', { |
|
||||||
'd': 'M 0 %s h%s' % ( self.graph_height, self.graph_width ), |
|
||||||
'class': 'axis', |
|
||||||
'id': 'yAxis' |
|
||||||
} ) ) |
|
||||||
|
|
||||||
self.draw_x_labels() |
|
||||||
self.draw_y_labels() |
|
||||||
|
|
||||||
def x_label_offset( self, width ): |
|
||||||
"""Where in the X area the label is drawn |
|
||||||
Centered in the field, should be width/2. Start, 0.""" |
|
||||||
return 0 |
|
||||||
|
|
||||||
def make_datapoint_text( self, x, y, value, style='' ): |
|
||||||
if self.show_data_values: |
|
||||||
e = self._create_element( 'text', { |
|
||||||
'x': str( x ), |
|
||||||
'y': str( y ), |
|
||||||
'class': 'dataPointLabel', |
|
||||||
'style': '%(style)s stroke: #fff; stroke-width: 2;' % vars(), |
|
||||||
} ) |
|
||||||
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 ): |
|
||||||
"Draw the X axis labels" |
|
||||||
if self.show_x_labels: |
|
||||||
labels = self.get_x_labels() |
|
||||||
count = len( labels ) |
|
||||||
|
|
||||||
labels = enumerate( iter( labels ) ) |
|
||||||
start = int( not self.step_include_first_x_label ) |
|
||||||
labels = islice( labels, start, None, self.step_x_labels ) |
|
||||||
map( self.draw_x_label, labels ) |
|
||||||
self.draw_x_guidelines( self.field_width(), count ) |
|
||||||
|
|
||||||
def draw_x_label( self, label ): |
|
||||||
label_width = self.field_width() |
|
||||||
index, label = label |
|
||||||
text = self._create_element( 'text', { 'class': 'xAxisLabels' } ) |
|
||||||
text.appendChild( self._doc.createTextNode( label ) ) |
|
||||||
self.graph.appendChild( text ) |
|
||||||
|
|
||||||
x = index * label_width + self.x_label_offset( label_width ) |
|
||||||
y = self.graph_height + self.x_label_font_size + 3 |
|
||||||
t = 0 - ( self.font_size / 2 ) |
|
||||||
|
|
||||||
if self.stagger_x_labels and (index % 2 ): |
|
||||||
stagger = self.x_label_font_size + 5 |
|
||||||
y += stagger |
|
||||||
graph_height = self.graph_height |
|
||||||
path = self._create_element( 'path', { |
|
||||||
'd': 'M%(x)f %(graph_height)f v%(stagger)d' % vars(), |
|
||||||
'class': 'staggerGuideLine' |
|
||||||
} ) |
|
||||||
self.graph.appendChild( path ) |
|
||||||
|
|
||||||
text.setAttribute( 'x', str( x ) ) |
|
||||||
text.setAttribute( 'y', str( y ) ) |
|
||||||
|
|
||||||
if self.rotate_x_labels: |
|
||||||
transform = 'rotate( 90 %d %d ) translate( 0 -%d )' % \ |
|
||||||
( x, y-self.x_label_font_size, x_label_font_size/4 ) |
|
||||||
text.setAttribute( 'transform', transform ) |
|
||||||
text.setAttribute( 'style', 'text-anchor: start' ) |
|
||||||
else: |
|
||||||
text.setAttribute( 'style', 'text-anchor: middle' ) |
|
||||||
|
|
||||||
def y_label_offset( self, height ): |
|
||||||
"""Where in the Y area the label is drawn |
|
||||||
Centered in the field, should be width/2. Start, 0.""" |
|
||||||
return 0 |
|
||||||
|
|
||||||
def get_field_width( self ): |
|
||||||
return float( self.graph_width - self.font_size*2*self.right_font ) / \ |
|
||||||
( len( self.get_x_labels() ) - self.right_align ) |
|
||||||
field_width = property( get_field_width ) |
|
||||||
|
|
||||||
def get_field_height( self ): |
|
||||||
return float( self.graph_height - self.font_size*2*self.top_font ) / \ |
|
||||||
( len( self.get_y_labels() ) - self.top_align ) |
|
||||||
field_height = property( get_field_height ) |
|
||||||
|
|
||||||
def draw_y_labels( self ): |
|
||||||
"Draw the Y axis labels" |
|
||||||
if self.show_y_labels: |
|
||||||
labels = self.get_y_labels() |
|
||||||
count = len( labels ) |
|
||||||
|
|
||||||
labels = enumerate( iter( labels ) ) |
|
||||||
start = int( not self.step_include_first_y_label ) |
|
||||||
labels = islice( labels, start, None, self.step_y_labels ) |
|
||||||
map( self.draw_y_label, labels ) |
|
||||||
self.draw_y_guidelines( self.field_height(), count ) |
|
||||||
|
|
||||||
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 not self.rotate_y_labels: result += self.font_size/1.2 |
|
||||||
return result |
|
||||||
y_offset = property( get_y_offset ) |
|
||||||
|
|
||||||
def draw_y_label( self, label ): |
|
||||||
label_height = self.field_height() |
|
||||||
index, label = label |
|
||||||
text = self._create_element( 'text', { 'class': 'yAxisLabels' } ) |
|
||||||
text.appendChild( self._doc.createTextNode( label ) ) |
|
||||||
self.graph.appendChild( text ) |
|
||||||
|
|
||||||
y = self.y_offset - ( label_height * index ) |
|
||||||
x = {True: 0, False:-3}[self.rotate_y_labels] |
|
||||||
|
|
||||||
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)f %(y)f h%(stagger)d' % vars(), |
|
||||||
'class': 'staggerGuideLine' |
|
||||||
} ) |
|
||||||
self.graph.appendChild( path ) |
|
||||||
|
|
||||||
text.setAttribute( 'x', str( x ) ) |
|
||||||
text.setAttribute( 'y', str( y ) ) |
|
||||||
|
|
||||||
if self.rotate_y_labels: |
|
||||||
transform = 'translate( -%d 0 ) rotate ( 90 %d %d )' % \ |
|
||||||
( self.font_size, x, y ) |
|
||||||
text.setAttribute( 'transform', transform ) |
|
||||||
text.setAttribute( 'style', 'text-anchor: middle' ) |
|
||||||
else: |
|
||||||
text.setAttribute( 'y', str( y - self.y_label_font_size/2 ) ) |
|
||||||
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 |
|
||||||
stop = self.graph_height |
|
||||||
path = self._create_element( 'path', { |
|
||||||
'd': 'M %(start)s 0 v%(stop)s' % vars(), |
|
||||||
'class': 'guideLines' } ) |
|
||||||
self.graph.appendChild( path ) |
|
||||||
|
|
||||||
|
|
||||||
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': 'M 0 %(start)s h%(stop)s' % vars(), |
|
||||||
'class': 'guideLines' } ) |
|
||||||
self.graph.appendChild( path ) |
|
||||||
|
|
||||||
def draw_titles( self ): |
|
||||||
"Draws the graph title and subtitle" |
|
||||||
if self.show_graph_title: self.draw_graph_title() |
|
||||||
if self.show_graph_subtitle: self.draw_graph_subtitle() |
|
||||||
if self.show_x_title: self.draw_x_title() |
|
||||||
if self.show_y_title: self.draw_y_title() |
|
||||||
|
|
||||||
def draw_graph_title( self ): |
|
||||||
text = self._create_element( 'text', { |
|
||||||
'x': str( self.width / 2 ), |
|
||||||
'y': str( self.title_font_size ), |
|
||||||
'class': 'mainTitle' } ) |
|
||||||
text.appendChild( self._doc.createTextNode( self.graph_title ) ) |
|
||||||
self.root.appendChild( text ) |
|
||||||
|
|
||||||
def draw_graph_subtitle( self ): |
|
||||||
raise NotImplementedError |
|
||||||
|
|
||||||
def draw_x_title( self ): |
|
||||||
raise NotImplementedError |
|
||||||
|
|
||||||
def draw_y_title( self ): |
|
||||||
x = self.y_title_font_size |
|
||||||
if self.y_title_text_direction=='bt': |
|
||||||
x += 3 |
|
||||||
rotate = -90 |
|
||||||
else: |
|
||||||
x -= 3 |
|
||||||
rotate = 90 |
|
||||||
y = self.height / 2 |
|
||||||
text = self._create_element( 'text', { |
|
||||||
'x': str( x ), |
|
||||||
'y': str( y ), |
|
||||||
'class': 'yAxisTitle', |
|
||||||
} ) |
|
||||||
text.appendChild( self._doc.createTextNode( self.y_title ) ) |
|
||||||
text.setAttribute( 'transform', 'rotate( %(rotate)d, %(x)s, %(y)s )' % vars() ) |
|
||||||
self.root.appendChild( text ) |
|
||||||
|
|
||||||
def keys( self ): |
|
||||||
return map( itemgetter( 'title' ), self.data ) |
|
||||||
|
|
||||||
def draw_legend( self ): |
|
||||||
if self.key: |
|
||||||
group = self._create_element( 'g' ) |
|
||||||
self.root.appendChild( group ) |
|
||||||
|
|
||||||
for key_count, key_name in enumerate( self.keys() ): |
|
||||||
y_offset = ( self.KEY_BOX_SIZE * key_count ) + (key_count * 5 ) |
|
||||||
rect = self._create_element( 'rect', { |
|
||||||
'x': '0', |
|
||||||
'y': str( y_offset ), |
|
||||||
'width': str( self.KEY_BOX_SIZE ), |
|
||||||
'height': str( self.KEY_BOX_SIZE ), |
|
||||||
'class': 'key%s' % (key_count + 1), |
|
||||||
} ) |
|
||||||
group.appendChild( rect ) |
|
||||||
text = self._create_element( 'text', { |
|
||||||
'x': str( self.KEY_BOX_SIZE + 5 ), |
|
||||||
'y': str( y_offset + self.KEY_BOX_SIZE ), |
|
||||||
'class': 'keyText' } ) |
|
||||||
text.appendChild( self._doc.createTextNode( key_name ) ) |
|
||||||
group.appendChild( text ) |
|
||||||
|
|
||||||
if self.key_position == 'right': |
|
||||||
x_offset = self.graph_width + self.border_left + 10 |
|
||||||
y_offset = self.border_top + 20 |
|
||||||
if self.key_position == 'bottom': |
|
||||||
raise NotImplementedError |
|
||||||
group.setAttribute( 'transform', 'translate(%(x_offset)d %(y_offset)d)' % vars() ) |
|
||||||
|
|
||||||
def style( self ): |
|
||||||
"hard code the styles into the xml if style sheets are not used." |
|
||||||
if self.no_css: |
|
||||||
styles = self.parse_css() |
|
||||||
for node in xpath.Evaluate( '//*[@class]', self.root ): |
|
||||||
cl = node.getAttribute( 'class' ) |
|
||||||
style = styles[ cl ] |
|
||||||
if node.hasAttribute( 'style' ): |
|
||||||
style += node.getAtrtibute( 'style' ) |
|
||||||
node.setAttribute( 'style', style ) |
|
||||||
|
|
||||||
def parse_css( self ): |
|
||||||
"""Take a .css file (classes only please) and parse it into a dictionary |
|
||||||
of class/style pairs.""" |
|
||||||
css = self.get_style() |
|
||||||
result = {} |
|
||||||
for match in re.finditer( '^(?<names>\.(\w+)(?:\s*,\s*\.\w+)*)\s*\{(?<content>[^}]+)\}' ): |
|
||||||
names = match.group_dict()['names'] |
|
||||||
# apperantly, we're only interested in class names |
|
||||||
names = filter( None, re.split( '\s*,?\s*\.' ) ) |
|
||||||
content = match.group_dict()['content'] |
|
||||||
# convert all whitespace to |
|
||||||
content = re.sub( '\s+', ' ', content ) |
|
||||||
for name in names: |
|
||||||
result[name] = ';'.join( result[name], content ) |
|
||||||
return result |
|
||||||
|
|
||||||
def add_defs( self, defs ): |
|
||||||
"Override and place code to add defs here" |
|
||||||
pass |
|
||||||
|
|
||||||
def start_svg( self ): |
|
||||||
"Base SVG Document Creation" |
|
||||||
impl = dom.getDOMImplementation() |
|
||||||
#dt = impl.createDocumentType( 'svg', 'PUBLIC' |
|
||||||
self._doc = impl.createDocument( None, 'svg', None ) |
|
||||||
self.root = self._doc.documentElement |
|
||||||
if hasattr( self, 'style_sheet' ): |
|
||||||
pi = self._doc.createProcessingInstruction( 'xml-stylesheet', |
|
||||||
'href="%s" type="text/css"' % self.style_sheet ) |
|
||||||
attributes = { |
|
||||||
'width': str( self.width ), |
|
||||||
'height': str( self.height ), |
|
||||||
'viewBox': '0 0 %s %s' % ( self.width, self.height ), |
|
||||||
'xmlns': 'http://www.w3.org/2000/svg', |
|
||||||
'xmlns:xlink': 'http://www.w3.org/1999/xlink', |
|
||||||
'xmlns:a3': 'http://ns.adobe.com/AdobeSVGViewerExtensions/3.0/', |
|
||||||
'a3:scriptImplementation': 'Adobe' } |
|
||||||
map( lambda a: self.root.setAttribute( *a ), attributes.items() ) |
|
||||||
self.root.appendChild( self._doc.createComment( ' Created with SVG.Graph ' ) ) |
|
||||||
self.root.appendChild( self._doc.createComment( ' SVG.Graph by Jason R. Coombs ' ) ) |
|
||||||
self.root.appendChild( self._doc.createComment( ' Based on SVG::Graph by Sean E. Russel ' ) ) |
|
||||||
self.root.appendChild( self._doc.createComment( ' Based on Perl SVG:TT:Graph by Leo Lapworth & Stephan Morgan ' ) ) |
|
||||||
self.root.appendChild( self._doc.createComment( ' '+'/'*66 ) ) |
|
||||||
|
|
||||||
defs = self._create_element( 'defs' ) |
|
||||||
self.add_defs( defs ) |
|
||||||
self.root.appendChild( defs ) |
|
||||||
if not hasattr( self, 'style_sheet' ) and not self.no_css: |
|
||||||
self.root.appendChild( self._doc.createComment( ' include default stylesheet if none specified ' ) ) |
|
||||||
style = self._create_element( 'style', { 'type': 'text/css' } ) |
|
||||||
defs.appendChild( style ) |
|
||||||
style_data = self._doc.createCDATASection( self.get_style() ) |
|
||||||
style.appendChild( style_data ) |
|
||||||
|
|
||||||
self.root.appendChild( self._doc.createComment( 'SVG Background' ) ) |
|
||||||
rect = self._create_element( 'rect', { |
|
||||||
'width': str( self.width ), |
|
||||||
'height': str( self.height ), |
|
||||||
'x': '0', |
|
||||||
'y': '0', |
|
||||||
'class': 'svgBackground' } ) |
|
||||||
self.root.appendChild( rect ) |
|
||||||
|
|
||||||
def calculate_graph_dimensions( self ): |
|
||||||
self.calculate_left_margin() |
|
||||||
self.calculate_right_margin() |
|
||||||
self.calculate_bottom_margin() |
|
||||||
self.calculate_top_margin() |
|
||||||
self.graph_width = self.width - self.border_left - self.border_right |
|
||||||
self.graph_height = self.height - self.border_top - self.border_bottom |
|
||||||
|
|
||||||
def get_style( self ): |
|
||||||
result = """/* Copy from here for external style sheet */ |
|
||||||
.svgBackground{ |
|
||||||
fill:#ffffff; |
|
||||||
} |
|
||||||
.graphBackground{ |
|
||||||
fill:#f0f0f0; |
|
||||||
} |
|
||||||
|
|
||||||
/* graphs titles */ |
|
||||||
.mainTitle{ |
|
||||||
text-anchor: middle; |
|
||||||
fill: #000000; |
|
||||||
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)dpx; |
|
||||||
font-family: "Arial", sans-serif; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
|
|
||||||
.axis{ |
|
||||||
stroke: #000000; |
|
||||||
stroke-width: 1px; |
|
||||||
} |
|
||||||
|
|
||||||
.guideLines{ |
|
||||||
stroke: #666666; |
|
||||||
stroke-width: 1px; |
|
||||||
stroke-dasharray: 5 5; |
|
||||||
} |
|
||||||
|
|
||||||
.xAxisLabels{ |
|
||||||
text-anchor: middle; |
|
||||||
fill: #000000; |
|
||||||
font-size: %(x_label_font_size)dpx; |
|
||||||
font-family: "Arial", sans-serif; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
|
|
||||||
.yAxisLabels{ |
|
||||||
text-anchor: end; |
|
||||||
fill: #000000; |
|
||||||
font-size: %(y_label_font_size)dpx; |
|
||||||
font-family: "Arial", sans-serif; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
|
|
||||||
.xAxisTitle{ |
|
||||||
text-anchor: middle; |
|
||||||
fill: #ff0000; |
|
||||||
font-size: %(x_title_font_size)dpx; |
|
||||||
font-family: "Arial", sans-serif; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
|
|
||||||
.yAxisTitle{ |
|
||||||
fill: #ff0000; |
|
||||||
text-anchor: middle; |
|
||||||
font-size: %(y_title_font_size)dpx; |
|
||||||
font-family: "Arial", sans-serif; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
|
|
||||||
.dataPointLabel{ |
|
||||||
fill: #000000; |
|
||||||
text-anchor:middle; |
|
||||||
font-size: 10px; |
|
||||||
font-family: "Arial", sans-serif; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
|
|
||||||
.staggerGuideLine{ |
|
||||||
fill: none; |
|
||||||
stroke: #000000; |
|
||||||
stroke-width: 0.5px; |
|
||||||
} |
|
||||||
|
|
||||||
%%s |
|
||||||
|
|
||||||
.keyText{ |
|
||||||
fill: #000000; |
|
||||||
text-anchor:start; |
|
||||||
font-size: %(key_font_size)dpx; |
|
||||||
font-family: "Arial", sans-serif; |
|
||||||
font-weight: normal; |
|
||||||
} |
|
||||||
/* End copy for external style sheet */ |
|
||||||
""" % 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" |
|
||||||
node = self._doc.createElement( nodeName ) |
|
||||||
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__ ) |
|
||||||
|
|
||||||
import Plot, TimeSeries |
|
@ -1,23 +0,0 @@ |
|||||||
# -*- coding: UTF-8 -*- |
|
||||||
|
|
||||||
""" Setup script for building SVG distribution |
|
||||||
|
|
||||||
Copyright © 2005 Jason R. Coombs |
|
||||||
""" |
|
||||||
|
|
||||||
from distutils.core import setup |
|
||||||
|
|
||||||
__author__ = 'Jason R. Coombs <jaraco@sandia.gov>' |
|
||||||
__version__ = '$Rev: $'[6:-2] |
|
||||||
__svnauthor__ = '$Author: $'[9:-2] |
|
||||||
__date__ = '$Date: $'[7:-2] |
|
||||||
|
|
||||||
setup ( name = 'SVGChart', |
|
||||||
version = '1.0.1', |
|
||||||
description = 'Python SVG Charting Support', |
|
||||||
author = 'Jason R. Coombs', |
|
||||||
author_email = 'jaraco@sandia.gov', |
|
||||||
packages = ['SVG'], |
|
||||||
package_dir = { 'SVG':'.' }, |
|
||||||
script_args = ( 'bdist_wininst', ) |
|
||||||
) |
|
@ -1,44 +0,0 @@ |
|||||||
import sys, os |
|
||||||
#sys.path.insert( 0, 'c:\documents and settings\jaraco\my documents\projects\jaraco' ) |
|
||||||
import SVG |
|
||||||
from SVG import Plot |
|
||||||
reload( SVG ) |
|
||||||
reload( Plot ) |
|
||||||
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.x_label_format = '%d-%b' |
|
||||||
#g.max_y_value = 200 |
|
||||||
|
|
||||||
g.add_data( { 'data': [ '2005-12-21T00:00:00', 20, '2005-12-22T00:00:00', 21 ], 'title': 'foo' } ) |
|
||||||
|
|
||||||
res = g.burn() |
|
||||||
print g.field_width |
|
||||||
print g.font_size |
|
||||||
print g.right_font |
|
||||||
print g.right_align |
|
||||||
print g.get_x_labels() |
|
||||||
|
|
||||||
f = open( r'c:\timeseries.py.svg', 'w' ) |
|
||||||
f.write( res ) |
|
||||||
f.close() |
|
@ -1,45 +0,0 @@ |
|||||||
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( { |
|
||||||
:timescale_divisions => '4 hours', |
|
||||||
:stagger_x_labels => true, |
|
||||||
:x_label_format => '%d-%b', |
|
||||||
} ) |
|
||||||
g.add_data( { :data=> [ '2005-12-21T00:00:00', 20, '2005-12-22T00:00:00', 21 ], :title=> 'foo' } ) |
|
||||||
|
|
||||||
res = g.burn() |
|
||||||
print g.field_width |
|
||||||
print "\n" |
|
||||||
print g.inspect |
|
||||||
print g.get_x_labels.length |
|
||||||
print "\n" |
|
||||||
print g.right_align |
|
||||||
print "\n" |
|
||||||
print g.get_x_labels |
|
||||||
print "\n" |
|
||||||
f = File.new( 'c:\timeseries.rb.svg', 'w' ) |
|
||||||
f.write( res ) |
|
||||||
f.close() |
|
Loading…
Reference in new issue