Browse Source

Group bars and text for nice css effect

pull/8/head
Florian Mounier 13 years ago
parent
commit
23d9e40e3b
  1. 11
      pygal/bar.css
  2. 516
      pygal/bar.py
  3. 5
      pygal/graph.css
  4. 16
      pygal/graph.py

11
pygal/bar.css

@ -1,12 +1,21 @@
.bar .dataPointLabel {
fill-opacity: 0;
-webkit-transition: 250ms;
}
.bar:hover .dataPointLabel {
fill-opacity: 0.9;
fill: #000000;
}
.key, .fill { .key, .fill {
fill-opacity: 0.5; fill-opacity: 0.5;
stroke: none; stroke: none;
stroke-width: 1px; stroke-width: 1px;
-webkit-transition: 250ms;
} }
.fill:hover{ .fill:hover{
fill-opacity: 0.25; fill-opacity: 0.25;
-webkit-transition: 250ms;
} }
.key1,.fill1{ .key1,.fill1{

516
pygal/bar.py

@ -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; ")

5
pygal/graph.css

@ -72,17 +72,12 @@ Base styles for pygal.Graph
} }
.dataPointLabel{ .dataPointLabel{
fill: transparent;
text-anchor:middle; text-anchor:middle;
font-size: 10px; font-size: 10px;
font-family: "Arial", sans-serif; font-family: "Arial", sans-serif;
font-weight: normal; font-weight: normal;
} }
.dataPointLabel:hover{
fill: #000000;
}
.staggerGuideLine{ .staggerGuideLine{
fill: none; fill: none;
stroke: #000000; stroke: #000000;

16
pygal/graph.py

@ -337,24 +337,14 @@ class Graph(object):
# consider width/2 for centering the labels # consider width/2 for centering the labels
return 0 return 0
def make_datapoint_text(self, x, y, value, style=None): def make_datapoint_text(self, group, x, y, value, style=None):
""" """
Add text for a datapoint Add text for a datapoint
""" """
if not self.show_data_values: if not self.show_data_values:
# do nothing
return return
# first lay down the text in a wide white stroke to
# differentiate it from the background e = etree.SubElement(group, 'text', {
e = etree.SubElement(self.foreground, 'text', {
'x': str(x),
'y': str(y),
'class': 'dataPointLabel',
'style': '%(style)s stroke: #fff; stroke-width: 2;' % vars(),
})
e.text = str(value)
# then lay down the text in the specified style
e = etree.SubElement(self.foreground, 'text', {
'x': str(x), 'x': str(x),
'y': str(y), 'y': str(y),
'class': 'dataPointLabel'}) 'class': 'dataPointLabel'})

Loading…
Cancel
Save