mirror of https://github.com/Kozea/pygal.git
jaraco
17 years ago
1 changed files with 419 additions and 0 deletions
@ -0,0 +1,419 @@ |
|||||||
|
#!python |
||||||
|
|
||||||
|
# $Id$ |
||||||
|
|
||||||
|
from operator import itemgetter, add |
||||||
|
from itools import flatten |
||||||
|
|
||||||
|
import SVG |
||||||
|
from Plot import float_range |
||||||
|
|
||||||
|
class Line(SVG.Graph): |
||||||
|
""" === Create presentation quality SVG line graphs easily |
||||||
|
|
||||||
|
= Synopsis |
||||||
|
|
||||||
|
require 'SVG/Graph/Line' |
||||||
|
|
||||||
|
fields = %w(Jan Feb Mar); |
||||||
|
data_sales_02 = [12, 45, 21] |
||||||
|
data_sales_03 = [15, 30, 40] |
||||||
|
|
||||||
|
graph = SVG::Graph::Line.new({ |
||||||
|
:height => 500, |
||||||
|
:width => 300, |
||||||
|
:fields => fields, |
||||||
|
}) |
||||||
|
|
||||||
|
graph.add_data({ |
||||||
|
:data => data_sales_02, |
||||||
|
:title => 'Sales 2002', |
||||||
|
}) |
||||||
|
|
||||||
|
graph.add_data({ |
||||||
|
:data => data_sales_03, |
||||||
|
:title => 'Sales 2003', |
||||||
|
}) |
||||||
|
|
||||||
|
print "Content-type: image/svg+xml\r\n\r\n"; |
||||||
|
print graph.burn(); |
||||||
|
|
||||||
|
= Description |
||||||
|
|
||||||
|
This object aims to allow you to easily create high quality |
||||||
|
SVG line graphs. 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/single.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. |
||||||
|
|
||||||
|
= See also |
||||||
|
|
||||||
|
* SVG::Graph::Graph |
||||||
|
* SVG::Graph::BarHorizontal |
||||||
|
* SVG::Graph::Bar |
||||||
|
* SVG::Graph::Pie |
||||||
|
* SVG::Graph::Plot |
||||||
|
* SVG::Graph::TimeSeries |
||||||
|
|
||||||
|
== Author |
||||||
|
|
||||||
|
Sean E. Russell <serATgermaneHYPHENsoftwareDOTcom> |
||||||
|
|
||||||
|
Copyright 2004 Sean E. Russell |
||||||
|
This software is available under the Ruby license[LICENSE.txt] |
||||||
|
|
||||||
|
""" |
||||||
|
|
||||||
|
"""Show a small circle on the graph where the line goes from one point to |
||||||
|
the next""" |
||||||
|
show_data_points = True |
||||||
|
show_data_values = True |
||||||
|
"""Accumulates each data set. (i.e. Each point increased by sum of all |
||||||
|
previous series at same point).""" |
||||||
|
stacked = False |
||||||
|
"Fill in the area under the plot" |
||||||
|
area_fill = False |
||||||
|
|
||||||
|
#override some defaults |
||||||
|
top_align = top_font = right_align = right_font = True |
||||||
|
|
||||||
|
def max_value(self): |
||||||
|
data = map(itemgetter('data'), self.data) |
||||||
|
if self.stacked: |
||||||
|
data = self.get_cumulative_data() |
||||||
|
return max(flatten(data)) |
||||||
|
|
||||||
|
def min_value(self): |
||||||
|
if self.min_scale_value: |
||||||
|
return self.min_scale_value |
||||||
|
data = map(itemgetter('data'), self.data) |
||||||
|
if self.stacked: |
||||||
|
data = self.get_cumulative_data() |
||||||
|
return min(flatten(data)) |
||||||
|
|
||||||
|
def get_cumulative_data(): |
||||||
|
"""Get the data as it will be charted. The first set will be |
||||||
|
the actual first data set. The second will be the sum of the |
||||||
|
first and the second, etc.""" |
||||||
|
sets = map(itemgetter('data'), self.data) |
||||||
|
if not sets: return |
||||||
|
sum = sets.pop(0) |
||||||
|
yield sum |
||||||
|
while sets: |
||||||
|
sum = map(add, sets.pop(0)) |
||||||
|
yield sum |
||||||
|
|
||||||
|
def get_x_labels(self): |
||||||
|
return self.fields |
||||||
|
|
||||||
|
def calculate_left_margin(self): |
||||||
|
super(self.__class__, self).calculate_left_margin() |
||||||
|
label_left = self.fields[0].length / 2 * self.font_size * 0.6 |
||||||
|
self.border_left = max(label_left, self.border_left) |
||||||
|
|
||||||
|
def get_y_labels(self): |
||||||
|
max_value = self.max_value() |
||||||
|
min_value = self.min_value() |
||||||
|
range = max_value - min_value |
||||||
|
top_pad = (range / 20.0) or 10 |
||||||
|
scale_range = (max_value + top_pad) - min_value |
||||||
|
|
||||||
|
scale_division = self.scale_divisions or (scale_range / 10.0) |
||||||
|
|
||||||
|
if self.scale_integers: |
||||||
|
scale_division = min(1, round(scale_division)) |
||||||
|
|
||||||
|
#maxvalue = maxvalue%scale_division == 0 ? |
||||||
|
# maxvalue : maxvalue + scale_division |
||||||
|
labels = tuple(float_range(min_value, max_value, scale_division)) |
||||||
|
return labels |
||||||
|
|
||||||
|
def calc_coords(self, field, value, width = None, height = None): |
||||||
|
if width is None: width = self.field_width |
||||||
|
if height is None: height = self.field_height |
||||||
|
coords = dict( |
||||||
|
x = width * field, |
||||||
|
y = self.graph_height - value * height, |
||||||
|
) |
||||||
|
return coords |
||||||
|
|
||||||
|
def draw_data(self): |
||||||
|
min_value = self.min_value() |
||||||
|
field_height = self.graph_height - self.font_size*2*self.top_font |
||||||
|
y_label_span = max(self.get_y_labels()) - min(self.get_y_labels()) |
||||||
|
field_height /= float(y_label_span) |
||||||
|
|
||||||
|
field_width = self.field_width |
||||||
|
#line = len(self.data) |
||||||
|
|
||||||
|
prev_sum = [0]*len(self.fields) |
||||||
|
cum_sum = [-min_value]*len(self.fields) |
||||||
|
|
||||||
|
coord_format = lambda c: '%(x)s %(y)s' % c |
||||||
|
|
||||||
|
for line_n, data in list(enumerate(self.data)).reversed(): |
||||||
|
apath = '' |
||||||
|
|
||||||
|
if not self.stacked: cum_sum = [-min_value]*len(self.fields) |
||||||
|
|
||||||
|
cum_sum = map(add, cum_sum, data['data']) |
||||||
|
get_coords = lambda (i, val): self.calc_coords(i, |
||||||
|
val, |
||||||
|
field_width, |
||||||
|
field_height) |
||||||
|
coords = map(get_coords, enumerate(cum_sum)) |
||||||
|
paths = map(coord_format, coords) |
||||||
|
line_path = ' '.join(paths) |
||||||
|
|
||||||
|
if self.area_fill: |
||||||
|
# to draw the area, we'll use the line above, followed by |
||||||
|
# tracing the bottom from right to left |
||||||
|
if self.stacked: |
||||||
|
prev_sum_rev = list(enumerate(prev_sum)).reversed() |
||||||
|
coords = map(get_coords, prev_sum_rev) |
||||||
|
paths = map(coord_format, coords) |
||||||
|
area_path = ' '.join(paths) |
||||||
|
origin = paths[-1] |
||||||
|
else: |
||||||
|
area_path = "V#@graph_height" |
||||||
|
origin = coord_format(get_coords(0,0)) |
||||||
|
|
||||||
|
p = self._create_element('path') |
||||||
|
d = ' '.join(( |
||||||
|
'M', |
||||||
|
origin, |
||||||
|
'L', |
||||||
|
line_path, |
||||||
|
area_path, |
||||||
|
'Z' |
||||||
|
) |
||||||
|
p.setAttribute('d', d) |
||||||
|
p.setAttribute('class', 'fill%(line_n)s' % vars()) |
||||||
|
self.graph.appendChild(p) |
||||||
|
|
||||||
|
# now draw the line itself |
||||||
|
p = self._create_element('path') |
||||||
|
p.setAttribute('d', 'M0 '+self.graph_height+' L'+line_path) |
||||||
|
p.setAttribute('class', 'line%(line_n)s' % vars()) |
||||||
|
self.graph.appendChild(p) |
||||||
|
|
||||||
|
if self.show_data_points or self.show_data_values: |
||||||
|
for i, value in enumerate(cum_sum): |
||||||
|
if self.show_data_points: |
||||||
|
circle = self._create_element( |
||||||
|
'circle', |
||||||
|
dict( |
||||||
|
cx = str(field_width*i), |
||||||
|
cy = str(self.graph_height - value*field_height), |
||||||
|
r = '2.5', |
||||||
|
) |
||||||
|
circle.setAttribute('class', 'dataPoint%(line_n)s' % vars()) |
||||||
|
self.graph.appendChild(circle) |
||||||
|
self.make_datapoint_text( |
||||||
|
field_width*i, |
||||||
|
self.graph_height - value*field_height - 6, |
||||||
|
value + min_value |
||||||
|
) |
||||||
|
|
||||||
|
prev_sum = list(cum_sum) |
||||||
|
|
||||||
|
|
||||||
|
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; |
||||||
|
} |
||||||
|
""" |
Loading…
Reference in new issue