mirror of https://github.com/Kozea/pygal.git
Python to generate nice looking SVG graph
http://pygal.org/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
518 lines
13 KiB
518 lines
13 KiB
17 years ago
|
#!/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;
|
||
|
}
|
||
|
"""
|