diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index 0c1532d..c4ba3e7 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -3,7 +3,7 @@ from pygal import ( Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, StackedLine, XY, CHARTS_BY_NAME, Config, Line, Histogram, Box, - Pie, Treemap, TimeLine, DateLine, + Pie, Treemap, TimeLine, DateLine, Radar, DateTimeLine) try: @@ -675,6 +675,29 @@ def get_test_routes(app): 'lol6', 'lol7', 'lol8', 'lol9', 'lol10', 'lol11'] return line.render_response() + @app.route('/test/radar') + def test_radar(): + radar = Radar() + for i in range(10): + radar.add(str(i), [i * j for j in range(8)]) + radar.x_labels = range(8) + radar.x_label_rotation = 35 + radar.y_label_rotation = 35 + radar.y_labels = [{ + 'label': '500', + 'value': 10 + }, { + 'label': '1000', + 'value': 20 + }, { + 'label': '5000', + 'value': 30 + }, { + 'label': '10000', + 'value': 40 + }] + return radar.render_response() + @app.route('/test/pie_serie_radius') def test_pie_serie_radius(): pie = Pie() diff --git a/docs/changelog.rst b/docs/changelog.rst index 4f1e776..b02eb57 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -12,6 +12,7 @@ Changelog * Default ``print_zeroes`` to True * (Re)Add xlink in desc to show on tooltip * Activate element on tooltip hovering. (#106) +* Fix radar axis behaviour (#247) 2.0.0 ===== diff --git a/pygal/css/graph.css b/pygal/css/graph.css index 2c1094b..7e93d53 100644 --- a/pygal/css/graph.css +++ b/pygal/css/graph.css @@ -23,7 +23,7 @@ } {{ id }}.guide.line { - fill-opacity: 0; + fill: none; } {{ id }}.centered { diff --git a/pygal/graph/base.py b/pygal/graph/base.py index edbc025..e3168fb 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -95,18 +95,12 @@ class BaseGraph(object): adapters.remove(fun) adapters = adapters + [positive, not_zero] adapters = adapters + [decimal_to_float] - adapter = reduce(compose, adapters) if not self.strict else ident - if adapter and self.y_labels: - self.y_labels = list(map(adapter, self.y_labels)) - - x_adapter = reduce( + self._adapt = reduce(compose, adapters) if not self.strict else ident + self._x_adapt = reduce( compose, self._x_adapters) if not self.strict and getattr( self, '_x_adapters', None) else ident - if x_adapter and self.x_labels: - self.x_labels = list(map(x_adapter, self.x_labels)) - series = [] raw = [( @@ -151,20 +145,22 @@ class BaseGraph(object): value = (value, self.zero, self.zero) elif len(value) == 2: value = (1, value[0], value[1]) - value = list(map(adapter, value)) + value = list(map(self._adapt, value)) elif self._dual: if value is None: value = (None, None) elif not is_list_like(value): value = (value, self.zero) - if x_adapter: - value = (x_adapter(value[0]), adapter(value[1])) + if self._x_adapt: + value = ( + self._x_adapt(value[0]), + self._adapt(value[1])) if isinstance(self, BaseMap): - value = (adapter(value[0]), value[1]) + value = (self._adapt(value[0]), value[1]) else: - value = list(map(adapter, value)) + value = list(map(self._adapt, value)) else: - value = adapter(value) + value = self._adapt(value) values.append(value) serie_config = SerieConfig() diff --git a/pygal/graph/dot.py b/pygal/graph/dot.py index fe1ce38..b0b35b2 100644 --- a/pygal/graph/dot.py +++ b/pygal/graph/dot.py @@ -26,6 +26,7 @@ from __future__ import division from math import log10 +from pygal._compat import to_str from pygal.graph.graph import Graph from pygal.util import alter, cached_property, decorate, safe_enumerate from pygal.view import ReverseView, View @@ -90,7 +91,7 @@ class Dot(Graph): def _compute_y_labels(self): self._y_labels = list(zip( - self.y_labels or [ + self.y_labels and map(to_str, self.y_labels) or [ serie.title['title'] if isinstance(serie.title, dict) else serie.title for serie in self.series], diff --git a/pygal/graph/dual.py b/pygal/graph/dual.py index 5c50292..4f0087c 100644 --- a/pygal/graph/dual.py +++ b/pygal/graph/dual.py @@ -42,7 +42,7 @@ class Dual(Graph): pos = x_pos[i] title = x_label else: - pos = float(x_label) + pos = self._x_adapt(float(x_label)) title = self._x_format(x_label) self._x_labels.append((title, pos)) diff --git a/pygal/graph/funnel.py b/pygal/graph/funnel.py index 8d68bf7..e38243e 100644 --- a/pygal/graph/funnel.py +++ b/pygal/graph/funnel.py @@ -96,9 +96,9 @@ class Funnel(Graph): self._x_labels = list( zip(self.x_labels and map(to_str, self.x_labels) or [ - serie.title['title'] - if isinstance(serie.title, dict) - else serie.title for serie in self.series], + serie.title['title'] + if isinstance(serie.title, dict) + else serie.title for serie in self.series], map(self._center, self._x_pos))) def _plot(self): diff --git a/pygal/graph/gauge.py b/pygal/graph/gauge.py index 40b08c2..c943eb2 100644 --- a/pygal/graph/gauge.py +++ b/pygal/graph/gauge.py @@ -161,7 +161,7 @@ class Gauge(Graph): else: pos = float(y_label) title = self._format(y_label) - self._y_labels.append((title, pos)) + self._y_labels.append((title, self._adapt(pos))) self.min_ = min(self.min_, min(cut(self._y_labels, 1))) self.max_ = max(self.max_, max(cut(self._y_labels, 1))) self._box.set_polar_box( diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index beb9dae..1e79cc3 100644 --- a/pygal/graph/graph.py +++ b/pygal/graph/graph.py @@ -766,7 +766,7 @@ class Graph(PublicApi): else: pos = float(y_label) title = self._format(y_label) - self._y_labels.append((title, pos)) + self._y_labels.append((title, self._adapt(pos))) self._box.ymin = min(self._box.ymin, min(cut(self._y_labels, 1))) self._box.ymax = max(self._box.ymax, max(cut(self._y_labels, 1))) else: diff --git a/pygal/graph/radar.py b/pygal/graph/radar.py index b7bce51..7cfc1ff 100644 --- a/pygal/graph/radar.py +++ b/pygal/graph/radar.py @@ -29,7 +29,9 @@ from math import cos, pi from pygal._compat import is_str from pygal.adapters import none_to_zero, positive from pygal.graph.line import Line -from pygal.util import cached_property, compute_scale, cut, deg, majorize +from pygal.util import ( + cached_property, compute_scale, cut, deg, truncate, + reverse_text_len) from pygal.view import PolarLogView, PolarView @@ -75,40 +77,43 @@ class Radar(Line): def _x_axis(self, draw_axes=True): """Override x axis to make it polar""" - if not self._x_labels: + if not self._x_labels or not self.show_x_labels: return - axis = self.svg.node(self.nodes['plot'], class_="axis x web") + axis = self.svg.node(self.nodes['plot'], class_="axis x web%s" % ( + ' always_show' if self.show_x_guides else '' + )) format_ = lambda x: '%f %f' % x center = self.view((0, 0)) r = self._rmax - if self.x_labels_major: - x_labels_major = self.x_labels_major - elif self.x_labels_major_every: - x_labels_major = [self._x_labels[i][0] for i in range( - 0, len(self._x_labels), self.x_labels_major_every)] - elif self.x_labels_major_count: - label_count = len(self._x_labels) - major_count = self.x_labels_major_count - if (major_count >= label_count): - x_labels_major = [label[0] for label in self._x_labels] + truncation = self.truncate_label + if not truncation: + if self.x_label_rotation or len(self._x_labels) <= 1: + truncation = 25 else: - x_labels_major = [self._x_labels[ - int(i * label_count / major_count)][0] - for i in range(major_count)] - else: - x_labels_major = [] + first_label_position = self.view.x(self._x_labels[0][1]) or 0 + last_label_position = self.view.x(self._x_labels[-1][1]) or 0 + available_space = ( + last_label_position - first_label_position) / ( + len(self._x_labels) - 1) + truncation = reverse_text_len( + available_space, self.style.label_font_size) + truncation = max(truncation, 1) for label, theta in self._x_labels: - major = label in x_labels_major + major = label in self._x_major_labels if not (self.show_minor_x_labels or major): continue guides = self.svg.node(axis, class_='guides') end = self.view((r, theta)) + self.svg.node( guides, 'path', d='M%s L%s' % (format_(center), format_(end)), - class_='%sline' % ('major ' if major else '')) + class_='%s%sline' % ( + 'axis ' if label == "0" else '', + 'major ' if major else '')) + r_txt = (1 - self._box.__class__.margin) * self._box.ymax pos_text = self.view((r_txt, theta)) text = self.svg.node( @@ -116,55 +121,57 @@ class Radar(Line): x=pos_text[0], y=pos_text[1], class_='major' if major else '') - text.text = label + text.text = truncate(label, truncation) + if text.text != label: + self.svg.node(guides, 'title').text = label + else: + self.svg.node( + guides, 'title', + ).text = self._x_format(theta) + angle = - theta + pi / 2 if cos(angle) < 0: angle -= pi text.attrib['transform'] = 'rotate(%f %s)' % ( - deg(angle), format_(pos_text)) + self.x_label_rotation or deg(angle), format_(pos_text)) def _y_axis(self, draw_axes=True): """Override y axis to make it polar""" - if not self._y_labels: + if not self._y_labels or not self.show_y_labels: return axis = self.svg.node(self.nodes['plot'], class_="axis y web") - if self.y_labels_major: - y_labels_major = self.y_labels_major - elif self.y_labels_major_every: - y_labels_major = [self._y_labels[i][1] for i in range( - 0, len(self._y_labels), self.y_labels_major_every)] - elif self.y_labels_major_count: - label_count = len(self._y_labels) - major_count = self.y_labels_major_count - if (major_count >= label_count): - y_labels_major = [label[1] for label in self._y_labels] - else: - y_labels_major = [self._y_labels[ - int(i * (label_count - 1) / (major_count - 1))][1] - for i in range(major_count)] - else: - y_labels_major = majorize( - cut(self._y_labels, 1) - ) for label, r in reversed(self._y_labels): - major = r in y_labels_major + major = r in self._y_major_labels if not (self.show_minor_y_labels or major): continue - guides = self.svg.node(axis, class_='guides') - self.svg.line( - guides, [self.view((r, theta)) for theta in self._x_pos], - close=True, - class_='%sguide line' % ( - 'major ' if major else '')) + guides = self.svg.node(axis, class_='%sguides' % ( + 'logarithmic ' if self.logarithmic else '' + )) + if self.show_y_guides: + self.svg.line( + guides, [self.view((r, theta)) for theta in self._x_pos], + close=True, + class_='%sguide line' % ( + 'major ' if major else '')) x, y = self.view((r, self._x_pos[0])) - self.svg.node( + x -= 5 + text = self.svg.node( guides, 'text', - x=x - 5, + x=x, y=y, class_='major' if major else '' - ).text = label + ) + text.text = label + + if self.y_label_rotation: + text.attrib['transform'] = "rotate(%d %f %f)" % ( + self.y_label_rotation, x, y) + + self.svg.node( + guides, 'title', + ).text = self._format(r) def _compute(self): """Compute r min max and labels position""" @@ -209,7 +216,7 @@ class Radar(Line): else: pos = float(y_label) title = self._format(y_label) - self._y_labels.append((title, pos)) + self._y_labels.append((title, self._adapt(pos))) self._rmin = min(self._rmin, min(cut(self._y_labels, 1))) self._rmax = max(self._rmax, max(cut(self._y_labels, 1))) self._box.set_polar_box(self._rmin, self._rmax) diff --git a/pygal/graph/stackedbar.py b/pygal/graph/stackedbar.py index b3e227e..12d0bca 100644 --- a/pygal/graph/stackedbar.py +++ b/pygal/graph/stackedbar.py @@ -22,9 +22,10 @@ on top of the others instead of being displayed side by side. """ from __future__ import division -from pygal.graph.bar import Bar -from pygal.util import compute_scale, swap, ident + from pygal.adapters import none_to_zero +from pygal.graph.bar import Bar +from pygal.util import ident, swap class StackedBar(Bar):