diff --git a/CHANGELOG b/CHANGELOG index 76ed9fe..b6b1248 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,8 @@ V 2.0.0 UNRELEASED Add new Box plot modes and outliers and set extremes as default (#226 #121 #149) (thanks djezar) Add secondary_range option to set range for secondary values. (#203) Maps are now plugins, they are removed from pygal core and moved to packages (pygal_maps_world, pygal_maps_fr, pygal_maps_ch, ...) (#225) + Dot now supports negative values + Fix dot with log scale (#201) V 1.7.0 Remove DateY and replace it by real XY datetime, date, time and timedelta support. (#188) diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index 28c51d4..1c34e29 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -186,12 +186,12 @@ def get_test_routes(app): @app.route('/test/dot') def test_dot(): - dot = Dot() + dot = Dot(logarithmic=True) dot.x_labels = map(str, range(4)) dot.add('a', [1, lnk(3, 'Foo'), 5, 3]) - dot.add('b', [2, 2, 0, 2, .1]) - dot.add('c', [5, 1, 5, lnk(3, 'Bar')]) - dot.add('d', [5, 5, lnk(0, 'Babar'), 3]) + dot.add('b', [2, -2, 0, 2, .1]) + dot.add('c', [5, 1, 50, lnk(3, 'Bar')]) + dot.add('d', [-5, 5, lnk(0, 'Babar'), 3]) return dot.render_response() diff --git a/pygal/css/graph.css b/pygal/css/graph.css index 095e7bc..79e7089 100644 --- a/pygal/css/graph.css +++ b/pygal/css/graph.css @@ -107,6 +107,10 @@ stroke-width: 5px; } +{{ id }}.dot.negative { + fill: transparent; +} + {{ id }}.series text { stroke: none; } diff --git a/pygal/graph/dot.py b/pygal/graph/dot.py index 87751ea..4a67f99 100644 --- a/pygal/graph/dot.py +++ b/pygal/graph/dot.py @@ -22,32 +22,47 @@ Dot chart """ from __future__ import division -from pygal.util import decorate, cut, safe_enumerate -from pygal.adapters import positive +from pygal.util import decorate, cut, safe_enumerate, cached_property from pygal.graph.graph import Graph +from pygal.view import View, ReverseView +from math import log10 class Dot(Graph): """Dot graph""" - _adapters = [positive] - def dot(self, serie, r_max): """Draw a dot line""" serie_node = self.svg.serie(serie) view_values = list(map(self.view, serie.points)) for i, value in safe_enumerate(serie.values): x, y = view_values[i] - size = r_max * value - value = self._format(value) + + if self.logarithmic: + log10min = log10(self._min) - 1 + log10max = log10(self._max or 1) + + if value != 0: + size = r_max * ( + (log10(abs(value)) - log10min) / + (log10max - log10min) + ) + else: + size = 0 + else: + size = r_max * (abs(value) / (self._max or 1)) + metadata = serie.metadata.get(i) dots = decorate( self.svg, self.svg.node(serie_node['plot'], class_="dots"), metadata) - self.svg.node(dots, 'circle', cx=x, cy=y, r=size, - class_='dot reactive tooltip-trigger') + self.svg.node(dots, 'circle', + cx=x, cy=y, r=size, + class_='dot reactive tooltip-trigger' + ( + ' negative' if value < 0 else '')) + value = self._format(value) self._tooltip_data(dots, value, x, y, classes='centered') self._static_value(serie_node, value, x, y) @@ -69,10 +84,30 @@ class Dot(Graph): self._y_labels = list(zip( self.y_labels or cut(self.series, 'title'), y_pos)) + def _set_view(self): + """Assign a view to current graph""" + view_class = ReverseView if self.inverse_y_axis else View + + self.view = view_class( + self.width - self.margin_box.x, + self.height - self.margin_box.y, + self._box) + + @cached_property + def _values(self): + """Getter for series values (flattened)""" + return [abs(val) for val in super()._values if val != 0] + + @cached_property + def _max(self): + """Getter for the maximum series value""" + return (self.range[1] if (self.range and self.range[1] is not None) + else (max(map(abs, self._values)) if self._values else None)) + def _plot(self): r_max = min( self.view.x(1) - self.view.x(0), (self.view.y(0) or 0) - self.view.y(1)) / ( - 2 * (self._max or 1) * 1.05) + 2 * 1.05) for serie in self.series: self.dot(serie, r_max) diff --git a/pygal/view.py b/pygal/view.py index e0a6ff5..a088c61 100644 --- a/pygal/view.py +++ b/pygal/view.py @@ -314,8 +314,8 @@ class LogView(View): if y is None or y <= 0 or self.log10_ymax - self.log10_ymin == 0: return 0 return (self.height - self.height * - (log10(y) - self.log10_ymin) - / (self.log10_ymax - self.log10_ymin)) + (log10(y) - self.log10_ymin) / ( + self.log10_ymax - self.log10_ymin)) class XLogView(View):