mirror of https://github.com/Kozea/pygal.git
Florian Mounier
13 years ago
4 changed files with 272 additions and 276 deletions
@ -1,257 +1,259 @@ |
|||||||
#!python |
#!python |
||||||
from itertools import chain |
from itertools import chain |
||||||
from lxml import etree |
from lxml import etree |
||||||
from pygal.graph import Graph |
from pygal.graph import Graph |
||||||
|
|
||||||
__all__ = ('VerticalBar', 'HorizontalBar') |
__all__ = ('VerticalBar', 'HorizontalBar') |
||||||
|
|
||||||
|
|
||||||
class Bar(Graph): |
class Bar(Graph): |
||||||
"A superclass for bar-style graphs. Do not instantiate directly." |
"A superclass for bar-style graphs. Do not instantiate directly." |
||||||
|
|
||||||
# gap between bars |
# gap between bars |
||||||
bar_gap = True |
bar_gap = True |
||||||
# how to stack adjacent dataset series |
# how to stack adjacent dataset series |
||||||
# overlap - overlap bars with transparent colors |
# overlap - overlap bars with transparent colors |
||||||
# top - stack bars on top of one another |
# top - stack bars on top of one another |
||||||
# side - stack bars side-by-side |
# side - stack bars side-by-side |
||||||
stack = 'overlap' |
stack = 'overlap' |
||||||
|
|
||||||
scale_divisions = None |
scale_divisions = None |
||||||
|
|
||||||
stylesheet_names = Graph.stylesheet_names + ['bar.css'] |
stylesheet_names = Graph.stylesheet_names + ['bar.css'] |
||||||
|
|
||||||
def __init__(self, fields, *args, **kargs): |
def __init__(self, fields, *args, **kargs): |
||||||
self.fields = fields |
self.fields = fields |
||||||
super(Bar, self).__init__(*args, **kargs) |
super(Bar, self).__init__(*args, **kargs) |
||||||
|
|
||||||
# adapted from Plot |
# adapted from Plot |
||||||
def get_data_values(self): |
def get_data_values(self): |
||||||
min_value, max_value, scale_division = self.data_range() |
min_value, max_value, scale_division = self.data_range() |
||||||
result = tuple( |
result = tuple( |
||||||
float_range(min_value, max_value + scale_division, scale_division)) |
float_range(min_value, max_value + scale_division, scale_division)) |
||||||
if self.scale_integers: |
if self.scale_integers: |
||||||
result = map(int, result) |
result = map(int, result) |
||||||
return result |
return result |
||||||
|
|
||||||
# adapted from plot (very much like calling data_range('y')) |
# adapted from plot (very much like calling data_range('y')) |
||||||
def data_range(self): |
def data_range(self): |
||||||
min_value = self.data_min() |
min_value = self.data_min() |
||||||
max_value = self.data_max() |
max_value = self.data_max() |
||||||
range = max_value - min_value |
range = max_value - min_value |
||||||
|
|
||||||
data_pad = range / 20.0 or 10 |
data_pad = range / 20.0 or 10 |
||||||
scale_range = (max_value + data_pad) - min_value |
scale_range = (max_value + data_pad) - min_value |
||||||
|
|
||||||
scale_division = self.scale_divisions or (scale_range / 10.0) |
scale_division = self.scale_divisions or (scale_range / 10.0) |
||||||
|
|
||||||
if self.scale_integers: |
if self.scale_integers: |
||||||
scale_division = round(scale_division) or 1 |
scale_division = round(scale_division) or 1 |
||||||
|
|
||||||
return min_value, max_value, scale_division |
return min_value, max_value, scale_division |
||||||
|
|
||||||
def get_field_labels(self): |
def get_field_labels(self): |
||||||
return self.fields |
return self.fields |
||||||
|
|
||||||
def get_data_labels(self): |
def get_data_labels(self): |
||||||
return map(str, self.get_data_values()) |
return map(str, self.get_data_values()) |
||||||
|
|
||||||
def data_max(self): |
def data_max(self): |
||||||
return max(chain(*map(lambda set: set['data'], self.data))) |
return max(chain(*map(lambda set: set['data'], self.data))) |
||||||
# above is same as |
# above is same as |
||||||
# return max(map(lambda set: max(set['data']), self.data)) |
# return max(map(lambda set: max(set['data']), self.data)) |
||||||
|
|
||||||
def data_min(self): |
def data_min(self): |
||||||
if not getattr(self, 'min_scale_value') is None: |
if not getattr(self, 'min_scale_value') is None: |
||||||
return self.min_scale_value |
return self.min_scale_value |
||||||
min_value = min(chain(*map(lambda set: set['data'], self.data))) |
min_value = min(chain(*map(lambda set: set['data'], self.data))) |
||||||
min_value = min(min_value, 0) |
min_value = min(min_value, 0) |
||||||
return min_value |
return min_value |
||||||
|
|
||||||
def get_bar_gap(self, field_size): |
def get_bar_gap(self, field_size): |
||||||
bar_gap = 10 # default gap |
bar_gap = 10 # default gap |
||||||
if field_size < 10: |
if field_size < 10: |
||||||
# adjust for narrow fields |
# adjust for narrow fields |
||||||
bar_gap = field_size / 2 |
bar_gap = field_size / 2 |
||||||
# the following zero's out the gap if bar_gap is False |
# the following zero's out the gap if bar_gap is False |
||||||
bar_gap = int(self.bar_gap) * bar_gap |
bar_gap = int(self.bar_gap) * bar_gap |
||||||
return bar_gap |
return bar_gap |
||||||
|
|
||||||
|
|
||||||
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 float(start) |
yield float(start) |
||||||
start += step |
start += step |
||||||
|
|
||||||
|
|
||||||
class VerticalBar(Bar): |
class VerticalBar(Bar): |
||||||
""" # === Create presentation quality SVG bar graphs easily |
""" # === Create presentation quality SVG bar graphs easily |
||||||
# |
# |
||||||
# = Synopsis |
# = Synopsis |
||||||
# |
# |
||||||
# require 'SVG/Graph/Bar' |
# require 'SVG/Graph/Bar' |
||||||
# |
# |
||||||
# fields = %w(Jan Feb Mar); |
# fields = %w(Jan Feb Mar); |
||||||
# data_sales_02 = [12, 45, 21] |
# data_sales_02 = [12, 45, 21] |
||||||
# |
# |
||||||
# graph = SVG::Graph::Bar.new( |
# graph = SVG::Graph::Bar.new( |
||||||
# :height => 500, |
# :height => 500, |
||||||
# :width => 300, |
# :width => 300, |
||||||
# :fields => fields |
# :fields => fields |
||||||
# ) |
# ) |
||||||
# |
# |
||||||
# graph.add_data( |
# graph.add_data( |
||||||
# :data => data_sales_02, |
# :data => data_sales_02, |
||||||
# :title => 'Sales 2002' |
# :title => 'Sales 2002' |
||||||
# ) |
# ) |
||||||
# |
# |
||||||
# print "Content-type: image/svg+xml\r\n\r\n" |
# print "Content-type: image/svg+xml\r\n\r\n" |
||||||
# print graph.burn |
# print graph.burn |
||||||
# |
# |
||||||
# = Description |
# = Description |
||||||
# |
# |
||||||
# This object aims to allow you to easily create high quality |
# This object aims to allow you to easily create high quality |
||||||
# SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default |
# SVG[http://www.w3c.org/tr/svg bar graphs. You can either use the default |
||||||
# style sheet or supply your own. Either way there are many options which |
# 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 - |
# 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. |
# with or without a key, data elements at each point, title, subtitle etc. |
||||||
# |
# |
||||||
# = Notes |
# = Notes |
||||||
# |
# |
||||||
# The default stylesheet handles upto 12 data sets, if you |
# The default stylesheet handles upto 12 data sets, if you |
||||||
# use more you must create your own stylesheet and add the |
# use more you must create your own stylesheet and add the |
||||||
# additional settings for the extra data sets. You will know |
# additional settings for the extra data sets. You will know |
||||||
# if you go over 12 data sets as they will have no style and |
# if you go over 12 data sets as they will have no style and |
||||||
# be in black. |
# be in black. |
||||||
# |
# |
||||||
# = Examples |
# = Examples |
||||||
# |
# |
||||||
# * http://germane-software.com/repositories/public/SVG/test/test.rb |
# * http://germane-software.com/repositories/public/SVG/test/test.rb |
||||||
# |
# |
||||||
# = See also |
# = See also |
||||||
# |
# |
||||||
# * SVG::Graph::Graph |
# * SVG::Graph::Graph |
||||||
# * SVG::Graph::BarHorizontal |
# * SVG::Graph::BarHorizontal |
||||||
# * SVG::Graph::Line |
# * SVG::Graph::Line |
||||||
# * SVG::Graph::Pie |
# * SVG::Graph::Pie |
||||||
# * SVG::Graph::Plot |
# * SVG::Graph::Plot |
||||||
# * SVG::Graph::TimeSeries |
# * SVG::Graph::TimeSeries |
||||||
""" |
""" |
||||||
top_align = top_font = 1 |
top_align = top_font = 1 |
||||||
|
|
||||||
def get_x_labels(self): |
def get_x_labels(self): |
||||||
return self.get_field_labels() |
return self.get_field_labels() |
||||||
|
|
||||||
def get_y_labels(self): |
def get_y_labels(self): |
||||||
return self.get_data_labels() |
return self.get_data_labels() |
||||||
|
|
||||||
def x_label_offset(self, width): |
def x_label_offset(self, width): |
||||||
return width / 2.0 |
return width / 2.0 |
||||||
|
|
||||||
def draw_data(self): |
def draw_data(self): |
||||||
min_value = self.data_min() |
min_value = self.data_min() |
||||||
unit_size = ( |
unit_size = ( |
||||||
float(self.graph_height) - self.font_size * 2 * self.top_font) |
float(self.graph_height) - self.font_size * 2 * self.top_font) |
||||||
unit_size /= ( |
unit_size /= ( |
||||||
max(self.get_data_values()) - min(self.get_data_values())) |
max(self.get_data_values()) - min(self.get_data_values())) |
||||||
|
|
||||||
bar_gap = self.get_bar_gap(self.get_field_width()) |
bar_gap = self.get_bar_gap(self.get_field_width()) |
||||||
|
|
||||||
bar_width = self.get_field_width() - bar_gap |
bar_width = self.get_field_width() - bar_gap |
||||||
if self.stack == 'side': |
if self.stack == 'side': |
||||||
bar_width /= len(self.data) |
bar_width /= len(self.data) |
||||||
|
|
||||||
x_mod = (self.graph_width - bar_gap) / 2 |
x_mod = (self.graph_width - bar_gap) / 2 |
||||||
if self.stack == 'side': |
if self.stack == 'side': |
||||||
x_mod -= bar_width / 2 |
x_mod -= bar_width / 2 |
||||||
|
|
||||||
bottom = self.graph_height |
bottom = self.graph_height |
||||||
|
|
||||||
for field_count, field in enumerate(self.fields): |
for field_count, field in enumerate(self.fields): |
||||||
for dataset_count, dataset in enumerate(self.data): |
for dataset_count, dataset in enumerate(self.data): |
||||||
# cases (assume 0 = +ve): |
# cases (assume 0 = +ve): |
||||||
# value min length |
# value min length |
||||||
# +ve +ve value - min |
# +ve +ve value - min |
||||||
# +ve -ve value - 0 |
# +ve -ve value - 0 |
||||||
# -ve -ve value.abs - 0 |
# -ve -ve value.abs - 0 |
||||||
value = dataset['data'][field_count] |
value = dataset['data'][field_count] |
||||||
|
|
||||||
left = self.get_field_width() * field_count |
left = self.get_field_width() * field_count |
||||||
|
|
||||||
length = (abs(value) - max(min_value, 0)) * unit_size |
length = (abs(value) - max(min_value, 0)) * unit_size |
||||||
# top is 0 if value is negative |
# top is 0 if value is negative |
||||||
top = bottom - ((max(value, 0) - min_value) * unit_size) |
top = bottom - ((max(value, 0) - min_value) * unit_size) |
||||||
if self.stack == 'side': |
if self.stack == 'side': |
||||||
left += bar_width * dataset_count |
left += bar_width * dataset_count |
||||||
|
|
||||||
rect = etree.SubElement(self.graph, 'rect', { |
rect_group = etree.SubElement(self.graph, "g", |
||||||
'x': str(left), |
{'class': 'bar'}) |
||||||
'y': str(top), |
etree.SubElement(rect_group, 'rect', { |
||||||
'width': str(bar_width), |
'x': str(left), |
||||||
'height': str(length), |
'y': str(top), |
||||||
'class': 'fill fill%s' % (dataset_count + 1), |
'width': str(bar_width), |
||||||
}) |
'height': str(length), |
||||||
|
'class': 'fill fill%s' % (dataset_count + 1), |
||||||
self.make_datapoint_text( |
}) |
||||||
left + bar_width / 2.0, top - 6, value) |
|
||||||
|
self.make_datapoint_text( |
||||||
|
rect_group, left + bar_width / 2.0, top - 6, value) |
||||||
class HorizontalBar(Bar): |
|
||||||
rotate_y_labels = True |
|
||||||
show_x_guidelines = True |
class HorizontalBar(Bar): |
||||||
show_y_guidelines = False |
rotate_y_labels = True |
||||||
right_align = right_font = True |
show_x_guidelines = True |
||||||
|
show_y_guidelines = False |
||||||
def get_x_labels(self): |
right_align = right_font = True |
||||||
return self.get_data_labels() |
|
||||||
|
def get_x_labels(self): |
||||||
def get_y_labels(self): |
return self.get_data_labels() |
||||||
return self.get_field_labels() |
|
||||||
|
def get_y_labels(self): |
||||||
def y_label_offset(self, height): |
return self.get_field_labels() |
||||||
return height / -2.0 |
|
||||||
|
def y_label_offset(self, height): |
||||||
def draw_data(self): |
return height / -2.0 |
||||||
min_value = self.data_min() |
|
||||||
|
def draw_data(self): |
||||||
unit_size = float(self.graph_width) |
min_value = self.data_min() |
||||||
unit_size -= self.font_size * 2 * self.right_font |
|
||||||
unit_size /= max(self.get_data_values()) - min(self.get_data_values()) |
unit_size = float(self.graph_width) |
||||||
|
unit_size -= self.font_size * 2 * self.right_font |
||||||
bar_gap = self.get_bar_gap(self.get_field_height()) |
unit_size /= max(self.get_data_values()) - min(self.get_data_values()) |
||||||
|
|
||||||
bar_height = self.get_field_height() - bar_gap |
bar_gap = self.get_bar_gap(self.get_field_height()) |
||||||
if self.stack == 'side': |
|
||||||
bar_height /= len(self.data) |
bar_height = self.get_field_height() - bar_gap |
||||||
|
if self.stack == 'side': |
||||||
y_mod = (bar_height / 2) + (self.font_size / 2) |
bar_height /= len(self.data) |
||||||
|
|
||||||
for field_count, field in enumerate(self.fields): |
y_mod = (bar_height / 2) + (self.font_size / 2) |
||||||
for dataset_count, dataset in enumerate(self.data): |
|
||||||
value = dataset['data'][field_count] |
for field_count, field in enumerate(self.fields): |
||||||
|
for dataset_count, dataset in enumerate(self.data): |
||||||
top = self.graph_height - ( |
value = dataset['data'][field_count] |
||||||
self.get_field_height() * (field_count + 1)) |
|
||||||
if self.stack == 'side': |
top = self.graph_height - ( |
||||||
top += (bar_height * dataset_count) |
self.get_field_height() * (field_count + 1)) |
||||||
# cases (assume 0 = +ve): |
if self.stack == 'side': |
||||||
# value min length left |
top += (bar_height * dataset_count) |
||||||
# +ve +ve value.abs - min minvalue.abs |
# cases (assume 0 = +ve): |
||||||
# +ve -ve value.abs - 0 minvalue.abs |
# value min length left |
||||||
# -ve -ve value.abs - 0 minvalue.abs + value |
# +ve +ve value.abs - min minvalue.abs |
||||||
length = (abs(value) - max(min_value, 0)) * unit_size |
# +ve -ve value.abs - 0 minvalue.abs |
||||||
# left is 0 if value is negative |
# -ve -ve value.abs - 0 minvalue.abs + value |
||||||
left = (abs(min_value) + min(value, 0)) * unit_size |
length = (abs(value) - max(min_value, 0)) * unit_size |
||||||
|
# left is 0 if value is negative |
||||||
rect = etree.SubElement(self.graph, 'rect', { |
left = (abs(min_value) + min(value, 0)) * unit_size |
||||||
'x': str(left), |
|
||||||
'y': str(top), |
rect = etree.SubElement(self.graph, 'rect', { |
||||||
'width': str(length), |
'x': str(left), |
||||||
'height': str(bar_height), |
'y': str(top), |
||||||
'class': 'fill%s' % (dataset_count + 1), |
'width': str(length), |
||||||
}) |
'height': str(bar_height), |
||||||
|
'class': 'fill fill%s' % (dataset_count + 1), |
||||||
self.make_datapoint_text( |
}) |
||||||
left + length + 5, top + y_mod, value, |
|
||||||
"text-anchor: start; ") |
self.make_datapoint_text( |
||||||
|
left + length + 5, top + y_mod, value, |
||||||
|
"text-anchor: start; ") |
||||||
|
Loading…
Reference in new issue