diff --git a/pygal/bar.css b/pygal/bar.css index a2dcde2..ba7e11d 100644 --- a/pygal/bar.css +++ b/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 { fill-opacity: 0.5; stroke: none; stroke-width: 1px; + -webkit-transition: 250ms; } .fill:hover{ fill-opacity: 0.25; - -webkit-transition: 250ms; } .key1,.fill1{ diff --git a/pygal/bar.py b/pygal/bar.py index 75b0b7e..d1488c4 100644 --- a/pygal/bar.py +++ b/pygal/bar.py @@ -1,257 +1,259 @@ -#!python -from itertools import chain -from lxml import etree -from pygal.graph import Graph - -__all__ = ('VerticalBar', 'HorizontalBar') - - -class Bar(Graph): - "A superclass for bar-style graphs. Do not instantiate directly." - - # gap between bars - bar_gap = True - # how to stack adjacent dataset series - # overlap - overlap bars with transparent colors - # top - stack bars on top of one another - # side - stack bars side-by-side - stack = 'overlap' - - scale_divisions = None - - stylesheet_names = Graph.stylesheet_names + ['bar.css'] - - def __init__(self, fields, *args, **kargs): - self.fields = fields - super(Bar, self).__init__(*args, **kargs) - - # adapted from Plot - def get_data_values(self): - min_value, max_value, scale_division = self.data_range() - result = tuple( - float_range(min_value, max_value + scale_division, scale_division)) - if self.scale_integers: - result = map(int, result) - return result - - # adapted from plot (very much like calling data_range('y')) - def data_range(self): - min_value = self.data_min() - max_value = self.data_max() - range = max_value - min_value - - data_pad = range / 20.0 or 10 - scale_range = (max_value + data_pad) - min_value - - scale_division = self.scale_divisions or (scale_range / 10.0) - - if self.scale_integers: - scale_division = round(scale_division) or 1 - - return min_value, max_value, scale_division - - def get_field_labels(self): - return self.fields - - def get_data_labels(self): - return map(str, self.get_data_values()) - - def data_max(self): - return max(chain(*map(lambda set: set['data'], self.data))) - # above is same as - # return max(map(lambda set: max(set['data']), self.data)) - - def data_min(self): - if not getattr(self, 'min_scale_value') is None: - return self.min_scale_value - min_value = min(chain(*map(lambda set: set['data'], self.data))) - min_value = min(min_value, 0) - return min_value - - def get_bar_gap(self, field_size): - bar_gap = 10 # default gap - if field_size < 10: - # adjust for narrow fields - bar_gap = field_size / 2 - # the following zero's out the gap if bar_gap is False - bar_gap = int(self.bar_gap) * bar_gap - return bar_gap - - -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 VerticalBar(Bar): - """ # === Create presentation quality SVG bar graphs easily - # - # = Synopsis - # - # require 'SVG/Graph/Bar' - # - # fields = %w(Jan Feb Mar); - # data_sales_02 = [12, 45, 21] - # - # graph = SVG::Graph::Bar.new( - # :height => 500, - # :width => 300, - # :fields => fields - # ) - # - # graph.add_data( - # :data => data_sales_02, - # :title => 'Sales 2002' - # ) - # - # 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[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 - # 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. - # - # = Notes - # - # The default stylesheet handles upto 12 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 12 data sets as they will have no style and - # be in black. - # - # = Examples - # - # * http://germane-software.com/repositories/public/SVG/test/test.rb - # - # = See also - # - # * SVG::Graph::Graph - # * SVG::Graph::BarHorizontal - # * SVG::Graph::Line - # * SVG::Graph::Pie - # * SVG::Graph::Plot - # * SVG::Graph::TimeSeries -""" - top_align = top_font = 1 - - def get_x_labels(self): - return self.get_field_labels() - - def get_y_labels(self): - return self.get_data_labels() - - def x_label_offset(self, width): - return width / 2.0 - - def draw_data(self): - min_value = self.data_min() - unit_size = ( - float(self.graph_height) - self.font_size * 2 * self.top_font) - unit_size /= ( - max(self.get_data_values()) - min(self.get_data_values())) - - bar_gap = self.get_bar_gap(self.get_field_width()) - - bar_width = self.get_field_width() - bar_gap - if self.stack == 'side': - bar_width /= len(self.data) - - x_mod = (self.graph_width - bar_gap) / 2 - if self.stack == 'side': - x_mod -= bar_width / 2 - - bottom = self.graph_height - - for field_count, field in enumerate(self.fields): - for dataset_count, dataset in enumerate(self.data): - # cases (assume 0 = +ve): - # value min length - # +ve +ve value - min - # +ve -ve value - 0 - # -ve -ve value.abs - 0 - value = dataset['data'][field_count] - - left = self.get_field_width() * field_count - - length = (abs(value) - max(min_value, 0)) * unit_size - # top is 0 if value is negative - top = bottom - ((max(value, 0) - min_value) * unit_size) - if self.stack == 'side': - left += bar_width * dataset_count - - rect = etree.SubElement(self.graph, 'rect', { - 'x': str(left), - 'y': str(top), - '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) - - -class HorizontalBar(Bar): - rotate_y_labels = True - show_x_guidelines = True - show_y_guidelines = False - right_align = right_font = True - - def get_x_labels(self): - return self.get_data_labels() - - def get_y_labels(self): - return self.get_field_labels() - - def y_label_offset(self, height): - return height / -2.0 - - def draw_data(self): - min_value = self.data_min() - - unit_size = float(self.graph_width) - unit_size -= self.font_size * 2 * self.right_font - unit_size /= max(self.get_data_values()) - min(self.get_data_values()) - - bar_gap = self.get_bar_gap(self.get_field_height()) - - bar_height = self.get_field_height() - bar_gap - if self.stack == 'side': - bar_height /= len(self.data) - - y_mod = (bar_height / 2) + (self.font_size / 2) - - for field_count, field in enumerate(self.fields): - for dataset_count, dataset in enumerate(self.data): - value = dataset['data'][field_count] - - top = self.graph_height - ( - self.get_field_height() * (field_count + 1)) - if self.stack == 'side': - top += (bar_height * dataset_count) - # cases (assume 0 = +ve): - # value min length left - # +ve +ve value.abs - min minvalue.abs - # +ve -ve value.abs - 0 minvalue.abs - # -ve -ve value.abs - 0 minvalue.abs + value - length = (abs(value) - max(min_value, 0)) * unit_size - # left is 0 if value is negative - left = (abs(min_value) + min(value, 0)) * unit_size - - rect = etree.SubElement(self.graph, 'rect', { - 'x': str(left), - 'y': str(top), - 'width': str(length), - 'height': str(bar_height), - 'class': 'fill%s' % (dataset_count + 1), - }) - - self.make_datapoint_text( - left + length + 5, top + y_mod, value, - "text-anchor: start; ") +#!python +from itertools import chain +from lxml import etree +from pygal.graph import Graph + +__all__ = ('VerticalBar', 'HorizontalBar') + + +class Bar(Graph): + "A superclass for bar-style graphs. Do not instantiate directly." + + # gap between bars + bar_gap = True + # how to stack adjacent dataset series + # overlap - overlap bars with transparent colors + # top - stack bars on top of one another + # side - stack bars side-by-side + stack = 'overlap' + + scale_divisions = None + + stylesheet_names = Graph.stylesheet_names + ['bar.css'] + + def __init__(self, fields, *args, **kargs): + self.fields = fields + super(Bar, self).__init__(*args, **kargs) + + # adapted from Plot + def get_data_values(self): + min_value, max_value, scale_division = self.data_range() + result = tuple( + float_range(min_value, max_value + scale_division, scale_division)) + if self.scale_integers: + result = map(int, result) + return result + + # adapted from plot (very much like calling data_range('y')) + def data_range(self): + min_value = self.data_min() + max_value = self.data_max() + range = max_value - min_value + + data_pad = range / 20.0 or 10 + scale_range = (max_value + data_pad) - min_value + + scale_division = self.scale_divisions or (scale_range / 10.0) + + if self.scale_integers: + scale_division = round(scale_division) or 1 + + return min_value, max_value, scale_division + + def get_field_labels(self): + return self.fields + + def get_data_labels(self): + return map(str, self.get_data_values()) + + def data_max(self): + return max(chain(*map(lambda set: set['data'], self.data))) + # above is same as + # return max(map(lambda set: max(set['data']), self.data)) + + def data_min(self): + if not getattr(self, 'min_scale_value') is None: + return self.min_scale_value + min_value = min(chain(*map(lambda set: set['data'], self.data))) + min_value = min(min_value, 0) + return min_value + + def get_bar_gap(self, field_size): + bar_gap = 10 # default gap + if field_size < 10: + # adjust for narrow fields + bar_gap = field_size / 2 + # the following zero's out the gap if bar_gap is False + bar_gap = int(self.bar_gap) * bar_gap + return bar_gap + + +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 VerticalBar(Bar): + """ # === Create presentation quality SVG bar graphs easily + # + # = Synopsis + # + # require 'SVG/Graph/Bar' + # + # fields = %w(Jan Feb Mar); + # data_sales_02 = [12, 45, 21] + # + # graph = SVG::Graph::Bar.new( + # :height => 500, + # :width => 300, + # :fields => fields + # ) + # + # graph.add_data( + # :data => data_sales_02, + # :title => 'Sales 2002' + # ) + # + # 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[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 + # 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. + # + # = Notes + # + # The default stylesheet handles upto 12 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 12 data sets as they will have no style and + # be in black. + # + # = Examples + # + # * http://germane-software.com/repositories/public/SVG/test/test.rb + # + # = See also + # + # * SVG::Graph::Graph + # * SVG::Graph::BarHorizontal + # * SVG::Graph::Line + # * SVG::Graph::Pie + # * SVG::Graph::Plot + # * SVG::Graph::TimeSeries +""" + top_align = top_font = 1 + + def get_x_labels(self): + return self.get_field_labels() + + def get_y_labels(self): + return self.get_data_labels() + + def x_label_offset(self, width): + return width / 2.0 + + def draw_data(self): + min_value = self.data_min() + unit_size = ( + float(self.graph_height) - self.font_size * 2 * self.top_font) + unit_size /= ( + max(self.get_data_values()) - min(self.get_data_values())) + + bar_gap = self.get_bar_gap(self.get_field_width()) + + bar_width = self.get_field_width() - bar_gap + if self.stack == 'side': + bar_width /= len(self.data) + + x_mod = (self.graph_width - bar_gap) / 2 + if self.stack == 'side': + x_mod -= bar_width / 2 + + bottom = self.graph_height + + for field_count, field in enumerate(self.fields): + for dataset_count, dataset in enumerate(self.data): + # cases (assume 0 = +ve): + # value min length + # +ve +ve value - min + # +ve -ve value - 0 + # -ve -ve value.abs - 0 + value = dataset['data'][field_count] + + left = self.get_field_width() * field_count + + length = (abs(value) - max(min_value, 0)) * unit_size + # top is 0 if value is negative + top = bottom - ((max(value, 0) - min_value) * unit_size) + if self.stack == 'side': + left += bar_width * dataset_count + + rect_group = etree.SubElement(self.graph, "g", + {'class': 'bar'}) + etree.SubElement(rect_group, 'rect', { + 'x': str(left), + 'y': str(top), + 'width': str(bar_width), + 'height': str(length), + 'class': 'fill fill%s' % (dataset_count + 1), + }) + + 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 + show_y_guidelines = False + right_align = right_font = True + + def get_x_labels(self): + return self.get_data_labels() + + def get_y_labels(self): + return self.get_field_labels() + + def y_label_offset(self, height): + return height / -2.0 + + def draw_data(self): + min_value = self.data_min() + + unit_size = float(self.graph_width) + unit_size -= self.font_size * 2 * self.right_font + unit_size /= max(self.get_data_values()) - min(self.get_data_values()) + + bar_gap = self.get_bar_gap(self.get_field_height()) + + bar_height = self.get_field_height() - bar_gap + if self.stack == 'side': + bar_height /= len(self.data) + + y_mod = (bar_height / 2) + (self.font_size / 2) + + for field_count, field in enumerate(self.fields): + for dataset_count, dataset in enumerate(self.data): + value = dataset['data'][field_count] + + top = self.graph_height - ( + self.get_field_height() * (field_count + 1)) + if self.stack == 'side': + top += (bar_height * dataset_count) + # cases (assume 0 = +ve): + # value min length left + # +ve +ve value.abs - min minvalue.abs + # +ve -ve value.abs - 0 minvalue.abs + # -ve -ve value.abs - 0 minvalue.abs + value + length = (abs(value) - max(min_value, 0)) * unit_size + # left is 0 if value is negative + left = (abs(min_value) + min(value, 0)) * unit_size + + rect = etree.SubElement(self.graph, 'rect', { + 'x': str(left), + 'y': str(top), + '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; ") diff --git a/pygal/graph.css b/pygal/graph.css index d2badf1..df33516 100644 --- a/pygal/graph.css +++ b/pygal/graph.css @@ -72,17 +72,12 @@ Base styles for pygal.Graph } .dataPointLabel{ - fill: transparent; text-anchor:middle; font-size: 10px; font-family: "Arial", sans-serif; font-weight: normal; } -.dataPointLabel:hover{ - fill: #000000; -} - .staggerGuideLine{ fill: none; stroke: #000000; diff --git a/pygal/graph.py b/pygal/graph.py index dd4d51c..c0c5049 100644 --- a/pygal/graph.py +++ b/pygal/graph.py @@ -337,24 +337,14 @@ class Graph(object): # consider width/2 for centering the labels 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 """ if not self.show_data_values: - # do nothing return - # first lay down the text in a wide white stroke to - # differentiate it from the background - 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', { + + e = etree.SubElement(group, 'text', { 'x': str(x), 'y': str(y), 'class': 'dataPointLabel'})