diff --git a/pygal/graph/bar.py b/pygal/graph/bar.py index 100bd97..d685227 100644 --- a/pygal/graph/bar.py +++ b/pygal/graph/bar.py @@ -102,27 +102,16 @@ class Bar(Graph): width=bar_inner_width, height=height, class_='rect reactive tooltip-trigger') - self.svg.node(bar, 'desc', class_="value").text = val - tooltip_positions = map( - str, (x + bar_inner_width / 2, y + height / 2)) - self.svg.node(bar, 'desc', - class_="x centered" - ).text = tooltip_positions[int(self.horizontal)] - self.svg.node(bar, 'desc', - class_="y centered" - ).text = tooltip_positions[int(not self.horizontal)] + + x += bar_inner_width / 2 + y += height / 2 + if self.horizontal: - x += .3 * self.value_font_size - y += height / 2 - else: - y += height / 2 + .3 * self.value_font_size - if self.print_values: - self.svg.transposable_node( - serie_node['text_overlay'], 'text', - class_='centered', - x=x + bar_inner_width / 2, - y=y - ).text = val if self.print_zeroes or val != '0' else '' + x, y = y, x + + self._tooltip_data(bar, val, x, y, classes="centered") + self._static_value(serie_node, val, x, y) + return stack_vals def _compute(self): diff --git a/pygal/graph/base.py b/pygal/graph/base.py index 2629f90..b87f90c 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -23,7 +23,7 @@ Base for pygal charts from __future__ import division import io -from pygal.serie import Serie +from pygal.serie import Serie, Value from pygal.view import Margin, Box from pygal.util import get_text_box, get_texts_box, cut, rad, humanize from pygal.svg import Svg @@ -35,6 +35,8 @@ from math import sin, cos class BaseGraph(object): """Graphs commons""" + __value__ = Value + def __init__(self, config=None, **kwargs): """Init the graph""" self.config = config or Config() @@ -51,7 +53,8 @@ class BaseGraph(object): def add(self, title, values): """Add a serie to this graph""" - self.series.append(Serie(title, values, len(self.series))) + self.series.append( + Serie(title, list(values), len(self.series), self.__value__)) def reinit(self): """(Re-)Init the graph""" @@ -119,6 +122,11 @@ class BaseGraph(object): """Getter for the maximum series size""" return max([len(serie.values) for serie in self.series]) + @cached_property + def _max(self): + """Getter for the maximum series value""" + return max(self._values) + def _draw(self): """Draw all the things""" self._compute() @@ -134,6 +142,13 @@ class BaseGraph(object): return False return True + def _uniformize_data(self): + """Make all series to max len""" + for serie in self.series: + if len(serie.values) < self._len: + diff = self._len - len(serie.values) + serie.metadata += diff * [self.__value__(0)] + def _render(self): """Make the graph internally""" self.reinit() diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index 2b7a6e4..164542d 100644 --- a/pygal/graph/graph.py +++ b/pygal/graph/graph.py @@ -36,11 +36,15 @@ class Graph(BaseGraph): """Draw all decorations""" self._set_view() self._make_graph() - self._x_axis() - self._y_axis() + self._axes() self._legend() self._title() + def _axes(self): + """Draw axes""" + self._x_axis() + self._y_axis() + def _set_view(self): """Assign a view to current graph""" self.view = (LogView if self.logarithmic else View)( @@ -92,14 +96,14 @@ class Graph(BaseGraph): self.svg.node(text, 'tspan', class_='label') self.svg.node(text, 'tspan', class_='value') - def _x_axis(self): + def _x_axis(self, draw_axes=True): """Make the x axis: labels and guides""" if not self._x_labels: return axis = self.svg.node(self.nodes['plot'], class_="axis x") - if 0 not in [label[1] for label in self._x_labels]: + if 0 not in [label[1] for label in self._x_labels] and draw_axes: self.svg.node(axis, 'path', d='M%f %f v%f' % (0, 0, self.view.height), class_='line') @@ -107,10 +111,12 @@ class Graph(BaseGraph): guides = self.svg.node(axis, class_='guides') x = self.view.x(position) y = self.view.height + 5 - self.svg.node(guides, 'path', - d='M%f %f v%f' % (x, 0, self.view.height), + if draw_axes: + self.svg.node( + guides, 'path', + d='M%f %f v%f' % (x, 0, self.view.height), class_='%sline' % ( - 'guide ' if position != 0 else '')) + 'guide ' if position != 0 else '')) text = self.svg.node(guides, 'text', x=x, y=y + .5 * self.label_font_size + 5 @@ -120,14 +126,14 @@ class Graph(BaseGraph): text.attrib['transform'] = "rotate(%d %f %f)" % ( self.x_label_rotation, x, y) - def _y_axis(self): + def _y_axis(self, draw_axes=True): """Make the y axis: labels and guides""" if not self._y_labels: return axis = self.svg.node(self.nodes['plot'], class_="axis y") - if 0 not in [label[1] for label in self._y_labels]: + if 0 not in [label[1] for label in self._y_labels] and draw_axes: self.svg.node(axis, 'path', d='M%f %f h%f' % (0, self.view.height, self.view.width), class_='line') @@ -138,11 +144,13 @@ class Graph(BaseGraph): )) x = -5 y = self.view.y(position) - self.svg.node(guides, 'path', - d='M%f %f h%f' % (0, y, self.view.width), - class_='%s%sline' % ( - 'major ' if major else '', - 'guide ' if position != 0 else '')) + if draw_axes: + self.svg.node( + guides, 'path', + d='M%f %f h%f' % (0, y, self.view.width), + class_='%s%sline' % ( + 'major ' if major else '', + 'guide ' if position != 0 else '')) text = self.svg.node(guides, 'text', x=x, y=y + .35 * self.label_font_size, @@ -226,3 +234,27 @@ class Graph(BaseGraph): coord = tuple(reversed(coord)) interpolateds.append(coord) return interpolateds + + def _tooltip_data(self, node, value, x, y, classes=None): + self.svg.node(node, 'desc', class_="value").text = value + if classes is None: + classes = [] + if x > self.view.width / 2: + classes.append('left') + if y > self.view.height / 2: + classes.append('top') + classes = ' '.join(classes) + + self.svg.node(node, 'desc', + class_="x " + classes).text = str(x) + self.svg.node(node, 'desc', + class_="y " + classes).text = str(y) + + def _static_value(self, serie_node, value, x, y): + if self.print_values: + self.svg.node( + serie_node['text_overlay'], 'text', + class_='centered', + x=x, + y=y + self.value_font_size / 3 + ).text = value if self.print_zeroes or value != '0' else '' diff --git a/pygal/graph/line.py b/pygal/graph/line.py index a486151..afc0ae2 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -75,17 +75,11 @@ class Line(Graph): val = self._get_value(serie.points, i) self.svg.node(dots, 'circle', cx=x, cy=y, r=2.5, class_='dot reactive tooltip-trigger') - self.svg.node(dots, 'desc', class_="value").text = val - self.svg.node(dots, 'desc', - class_="x " + classes).text = str(x) - self.svg.node(dots, 'desc', - class_="y " + classes).text = str(y) - if self.print_values: - self.svg.node( - serie_node['text_overlay'], 'text', - x=x + self.value_font_size, - y=y + self.value_font_size, - ).text = val + self._tooltip_data(dots, val, x, y) + self._static_value( + serie_node, val, + x + self.value_font_size, + y + self.value_font_size) if self.stroke: if self.interpolate: @@ -97,7 +91,7 @@ class Line(Graph): class_='line reactive' + (' nofill' if not self.fill else '')) def _compute(self): - x_pos = [x / float(self._len - 1) for x in range(self._len) + x_pos = [x / (self._len - 1) for x in range(self._len) ] if self._len != 1 else [.5] # Center if only one value for serie in self.series: if not hasattr(serie, 'points'): diff --git a/pygal/graph/pie.py b/pygal/graph/pie.py index bed33af..9b6f301 100644 --- a/pygal/graph/pie.py +++ b/pygal/graph/pie.py @@ -22,6 +22,7 @@ Pie chart """ from __future__ import division +from pygal.serie import PositiveValue from pygal.util import decorate from pygal.graph.graph import Graph from math import pi @@ -30,6 +31,8 @@ from math import pi class Pie(Graph): """Pie graph""" + __value__ = PositiveValue + def slice(self, serie_node, start_angle, serie, total): """Make a serie slice""" @@ -69,11 +72,6 @@ class Pie(Graph): original_start_angle, center, val) return serie_angle - def _compute(self): - for serie in self.series: - serie.values = map(lambda x: max(x, 0), serie.values) - return super(Pie, self)._compute() - def _plot(self): total = sum(map(sum, map(lambda x: x.values, self.series))) diff --git a/pygal/graph/radar.py b/pygal/graph/radar.py index ef3e58f..f6eb9e8 100644 --- a/pygal/graph/radar.py +++ b/pygal/graph/radar.py @@ -23,6 +23,7 @@ Radar chart from __future__ import division from pygal.graph.line import Line +from pygal.serie import PositiveValue from pygal.view import PolarView from pygal.util import deg, cached_property, compute_scale from math import cos, pi @@ -31,6 +32,8 @@ from math import cos, pi class Radar(Line): """Kiviat graph""" + __value__ = PositiveValue + def __init__(self, *args, **kwargs): self.x_pos = None self._rmax = None diff --git a/pygal/graph/stackedline.py b/pygal/graph/stackedline.py index 410735d..8779c3a 100644 --- a/pygal/graph/stackedline.py +++ b/pygal/graph/stackedline.py @@ -41,6 +41,7 @@ class StackedLine(Line): return new_values def _compute(self): + self._uniformize_data() x_pos = [x / float(self._len - 1) for x in range(self._len) ] if self._len != 1 else [.5] # Center if only one value accumulation = [0] * self._len diff --git a/pygal/serie.py b/pygal/serie.py index e21fa94..176b2d3 100644 --- a/pygal/serie.py +++ b/pygal/serie.py @@ -18,27 +18,41 @@ # along with pygal. If not, see . """ Little helpers for series + """ +from pygal.util import cut class Serie(object): """Serie containing title, values and the graph serie index""" - def __init__(self, title, values, index): + def __init__(self, title, values, index, value_class): self.title = title if isinstance(values, dict) or not hasattr(values, '__iter__'): values = [values] - self.metadata = map(Value, values) - self.values = [value.value for value in self.metadata] + self.metadata = map(value_class, values) self.index = index + @property + def values(self): + return cut(self.metadata, 'value') + class Value(object): + """Value container""" + def __init__(self, value): if not isinstance(value, dict): value = {'value': value} self.__dict__.update(value) +class PositiveValue(Value): + """Positive or zero value container""" + + def __init__(self, value): + super(PositiveValue, self).__init__(max(0, value)) + + class Label(object): """A label with his position""" def __init__(self, label, pos): diff --git a/pygal/svg.py b/pygal/svg.py index 2dd3cbc..12d15e3 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -152,21 +152,11 @@ class Svg(object): to[2], get_radius(small_radius), int(angle > pi), to[3]), class_='slice reactive tooltip-trigger') - self.node(node, 'desc', class_="value").text = val - tooltip_position = map( - str, diff(center, project( - (radius + small_radius) / 2, start_angle + angle / 2))) - self.node(node, 'desc', - class_="x centered").text = tooltip_position[0] - self.node(node, 'desc', - class_="y centered").text = tooltip_position[1] - if self.graph.print_values: - self.node( - serie_node['text_overlay'], 'text', - class_='centered', - x=tooltip_position[0], - y=tooltip_position[1] - ).text = val if self.graph.print_zeroes or val != '0%' else '' + x, y = diff(center, project( + (radius + small_radius) / 2, start_angle + angle / 2)) + + self.graph._tooltip_data(node, val, x, y, classes="centered") + self.graph._static_value(serie_node, val, x, y) def pre_render(self, no_data=False): """Last things to do before rendering"""