From 7748f847f37dd31bab7dcb244f41d1321f962d9f Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 22 Jul 2015 11:40:20 +0200 Subject: [PATCH] Refactor label processing in a ``_compute_x_labels`` and ``_compute_y_labels`` method. Handle both string and numbers for all charts. Create a ``Dual`` base chart for dual axis charts. (Fix #236) --- demo/moulinrouge/templates/svgs.jinja2 | 4 +- demo/moulinrouge/tests.py | 49 +++++++++++++++--- docs/api/pygal.graph.dual.rst | 7 +++ docs/api/pygal.graph.rst | 1 + docs/changelog.rst | 1 + docs/documentation/configuration/value.rst | 60 ++++++++++++++++++++++ pygal/graph/bar.py | 11 +--- pygal/graph/box.py | 13 +---- pygal/graph/dot.py | 18 +++++-- pygal/graph/dual.py | 53 +++++++++++++++++++ pygal/graph/funnel.py | 24 +++++---- pygal/graph/gauge.py | 42 ++++++++++++--- pygal/graph/graph.py | 47 ++++++++++++++--- pygal/graph/histogram.py | 17 +----- pygal/graph/line.py | 25 +-------- pygal/graph/map.py | 6 +++ pygal/graph/pie.py | 6 +++ pygal/graph/radar.py | 39 ++++++++++---- pygal/graph/stackedbar.py | 9 ---- pygal/graph/xy.py | 27 ++-------- pygal/test/test_line.py | 1 + pygal/util.py | 9 +--- 22 files changed, 318 insertions(+), 151 deletions(-) create mode 100644 docs/api/pygal.graph.dual.rst create mode 100644 pygal/graph/dual.py diff --git a/demo/moulinrouge/templates/svgs.jinja2 b/demo/moulinrouge/templates/svgs.jinja2 index 1501cf2..42846bd 100644 --- a/demo/moulinrouge/templates/svgs.jinja2 +++ b/demo/moulinrouge/templates/svgs.jinja2 @@ -1,10 +1,10 @@ {% extends '_layout.jinja2' %} {% block section %} - {% for svg in svgs %} + {% for svg in svgs | sort(attribute='type') %}
-
+
{{ svg.type }}
{% endfor %} {% endblock section %} diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index 01d0152..ebe2ae6 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -180,6 +180,12 @@ def get_test_routes(app): gauge.add('Need m', [-4]) gauge.add('Need z', [-10, 10.5]) gauge.add('No', [99, -99]) + gauge.y_labels = [ + {'label': 'X', + 'value': 6}, + {'label': '><', + 'value': -6} + ] return gauge.render_response() @app.route('/test/gauge/log') @@ -231,7 +237,7 @@ def get_test_routes(app): graph.add('2', [7, -4, 10, None, 8, 3, 1]) graph.add('3', [7, -14, -10, None, 8, 3, 1]) graph.add('4', [7, 4, -10, None, 8, 3, 1]) - graph.x_labels = ('a', 'b', 'c', 'd', 'e', 'f', 'g') + graph.x_labels = ('a', 'b', 'c', 'd') graph.x_label_rotation = 90 return graph.render_response() @@ -341,13 +347,24 @@ def get_test_routes(app): (1.5, 5, 10) ]) hist.add('2', [(2, 2, 8)]) + hist.x_labels = [0, 3, 6, 9, 12] return hist.render_response() @app.route('/test/ylabels') def test_ylabels(): - chart = Line() + chart = Bar() chart.x_labels = 'Red', 'Blue', 'Green' - chart.y_labels = .0001, .0003, .0004, .00045, .0005 + chart.y_labels = [ + {'value': .0001, + 'label': 'LOL'}, + {'value': .0003, + 'label': 'ROFL'}, + {'value': .0004, + 'label': 'MAO'}, + {'value': .00045, + 'label': 'LMFAO'}, + {'value': .0005, + 'label': 'GMCB'}] chart.add('line', [.0002, .0005, .00035]) return chart.render_response() @@ -398,6 +415,7 @@ def get_test_routes(app): stacked = StackedBar(stack_from_top=True) stacked.add('1', [1, 2, 3]) stacked.add('2', [4, 5, 6]) + stacked.x_labels = ['a', 'b', 'c'] return stacked.render_response() @app.route('/test/show_dots') @@ -603,9 +621,10 @@ def get_test_routes(app): @app.route('/test/x_major_labels/') def test_x_major_labels_for(chart): chart = CHARTS_BY_NAME[chart]() - chart.add('test', range(12)) - chart.x_labels = map(str, range(12)) - chart.x_labels_major_count = 4 + for i in range(12): + chart.add('test', range(12)) + chart.x_labels = map(str, range(10)) + # chart.x_labels_major_count = 4 # chart.x_labels_major = ['1', '5', '11', '1.0', '5.0', '11.0'] return chart.render_response() @@ -794,4 +813,22 @@ def get_test_routes(app): line.add('_', [1, 32, 12, .4, .009]) return line.render_response() + @app.route('/test/legendlink/') + def test_legend_link_for(chart): + chart = CHARTS_BY_NAME[chart]() + # link on chart and label + chart.add({ + 'title': 'Red', + 'xlink': {'href': 'http://en.wikipedia.org/wiki/Red'} + }, [{ + 'value': 2, + 'label': 'This is red', + 'xlink': {'href': 'http://en.wikipedia.org/wiki/Red'}}]) + + chart.add({'title': 'Yellow', 'xlink': { + 'href': 'http://en.wikipedia.org/wiki/Yellow', + 'target': '_blank'}}, 7) + + return chart.render_response() + return list(sorted(filter(lambda x: x.startswith('test'), locals()))) diff --git a/docs/api/pygal.graph.dual.rst b/docs/api/pygal.graph.dual.rst new file mode 100644 index 0000000..f830f3a --- /dev/null +++ b/docs/api/pygal.graph.dual.rst @@ -0,0 +1,7 @@ +pygal.graph.dual module +======================= + +.. automodule:: pygal.graph.dual + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/pygal.graph.rst b/docs/api/pygal.graph.rst index 3f1a604..05f78b1 100644 --- a/docs/api/pygal.graph.rst +++ b/docs/api/pygal.graph.rst @@ -15,6 +15,7 @@ Submodules pygal.graph.base pygal.graph.box pygal.graph.dot + pygal.graph.dual pygal.graph.funnel pygal.graph.gauge pygal.graph.graph diff --git a/docs/changelog.rst b/docs/changelog.rst index 0f7e06a..661d544 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -37,6 +37,7 @@ Changelog * Add ``tooltip_fancy_mode`` to revert to old tooltips * Add auto ``print_value`` color + a configurable ``value_colors`` list in style * Add ``guide_stroke_dasharray`` and ``guide_stroke_dasharray`` in style to customize guides (#242) (thanks cbergmiller) +* Refactor label processing in a ``_compute_x_labels`` and ``_compute_y_labels`` method. Handle both string and numbers for all charts. Create a ``Dual`` base chart for dual axis charts. (#236) 1.7.0 ===== diff --git a/docs/documentation/configuration/value.rst b/docs/documentation/configuration/value.rst index 8ad7e1d..12b7874 100644 --- a/docs/documentation/configuration/value.rst +++ b/docs/documentation/configuration/value.rst @@ -142,3 +142,63 @@ You can specify a dictionary to xlink with all links attributes: 'target': '_self'} }]) +Legend +~~~~~~ + +Finally legends can be link with the same mechanism: + + +.. pygal-code:: + + chart = pygal.Bar() + chart.add({ + 'title': 'First', + 'xlink': {'href': 'http://en.wikipedia.org/wiki/First'} + }, [{ + 'value': 2, + 'label': 'This is the first', + 'xlink': {'href': 'http://en.wikipedia.org/wiki/First'} + }]) + + chart.add({ + 'title': 'Second', + 'xlink': { + 'href': 'http://en.wikipedia.org/wiki/Second', + 'target': '_top' + } + }, [{ + 'value': 4, + 'label': 'This is the second', + 'xlink': { + 'href': 'http://en.wikipedia.org/wiki/Second', + 'target': '_top'} + }]) + + chart.add('Third', 7) + + chart.add({ + 'title': 'Fourth', + 'xlink': { + 'href': 'http://en.wikipedia.org/wiki/Fourth', + 'target': '_blank' + } + }, [{ + 'value': 5, + 'xlink': { + 'href': 'http://en.wikipedia.org/wiki/Fourth', + 'target': '_blank'} + }]) + + chart.add({ + 'title': 'Fifth', + 'xlink': { + 'href': 'http://en.wikipedia.org/wiki/Fifth', + 'target': '_self' + } + }, [{ + 'value': 3, + 'label': 'This is the fifth', + 'xlink': { + 'href': 'http://en.wikipedia.org/wiki/Fifth', + 'target': '_self'} + }]) diff --git a/pygal/graph/bar.py b/pygal/graph/bar.py index ed03661..f43c509 100644 --- a/pygal/graph/bar.py +++ b/pygal/graph/bar.py @@ -24,7 +24,7 @@ proportional to the values that they represent. from __future__ import division from pygal.graph.graph import Graph -from pygal.util import swap, ident, compute_scale, decorate, alter +from pygal.util import swap, ident, decorate, alter class Bar(Graph): @@ -104,15 +104,6 @@ class Bar(Graph): self._points(x_pos) - y_pos = compute_scale( - self._box.ymin, self._box.ymax, self.logarithmic, self.order_min, - self.min_scale, self.max_scale - ) if not self.y_labels else list(map(float, self.y_labels)) - - self._x_labels = self.x_labels and list(zip(self.x_labels, [ - (i + .5) / self._len for i in range(self._len)])) - self._y_labels = list(zip(map(self._format, y_pos), y_pos)) - def _compute_secondary(self): """Compute parameters for secondary series rendering""" if self.secondary_series: diff --git a/pygal/graph/box.py b/pygal/graph/box.py index 931d152..5f368c5 100644 --- a/pygal/graph/box.py +++ b/pygal/graph/box.py @@ -76,20 +76,9 @@ class Box(Graph): if self._max: self._box.ymax = max(self._max, self.zero) - x_pos = [ - x / self._len for x in range(self._len + 1) - ] if self._len > 1 else [0, 1] # Center if only one value - - self._points(x_pos) - - y_pos = compute_scale( - self._box.ymin, self._box.ymax, self.logarithmic, self.order_min, - self.min_scale, self.max_scale - ) if not self.y_labels else list(map(float, self.y_labels)) - + def _compute_x_labels(self): self._x_labels = self.x_labels and list(zip(self.x_labels, [ (i + .5) / self._order for i in range(self._order)])) - self._y_labels = list(zip(map(self._format, y_pos), y_pos)) def _plot(self): """Plot the series data""" diff --git a/pygal/graph/dot.py b/pygal/graph/dot.py index 6c82097..de09b4a 100644 --- a/pygal/graph/dot.py +++ b/pygal/graph/dot.py @@ -76,17 +76,25 @@ class Dot(Graph): self._box.xmax = x_len self._box.ymax = y_len - x_pos = [n / 2 for n in range(1, 2 * x_len, 2)] - y_pos = [n / 2 for n in reversed(range(1, 2 * y_len, 2))] + self._x_pos = [n / 2 for n in range(1, 2 * x_len, 2)] + self._y_pos = [n / 2 for n in reversed(range(1, 2 * y_len, 2))] for j, serie in enumerate(self.series): serie.points = [ - (x_pos[i], y_pos[j]) + (self._x_pos[i], self._y_pos[j]) for i in range(x_len)] - self._x_labels = self.x_labels and list(zip(self.x_labels, x_pos)) + def _compute_x_labels(self): + self._x_labels = self.x_labels and list( + zip(self.x_labels, self._x_pos)) + + def _compute_y_labels(self): self._y_labels = list(zip( - self.y_labels or cut(self.series, 'title'), y_pos)) + self.y_labels or [ + serie.title['title'] + if isinstance(serie.title, dict) + else serie.title for serie in self.series], + self._y_pos)) def _set_view(self): """Assign a view to current graph""" diff --git a/pygal/graph/dual.py b/pygal/graph/dual.py new file mode 100644 index 0000000..266e828 --- /dev/null +++ b/pygal/graph/dual.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# This file is part of pygal +# +# A python svg graph plotting library +# Copyright © 2012-2015 Kozea +# +# This library is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with pygal. If not, see . + +"""Dual chart base. Dual means a chart with 2 scaled axis like xy""" + +from pygal.util import cut, compute_scale +from pygal._compat import is_str +from pygal.graph.graph import Graph + + +class Dual(Graph): + _dual = True + + def _compute_x_labels(self): + x_pos = compute_scale( + self._box.xmin, self._box.xmax, self.logarithmic, + self.order_min, self.min_scale, self.max_scale + ) + if self.x_labels: + self._x_labels = [] + for i, x_label in enumerate(self.x_labels): + if isinstance(x_label, dict): + pos = float(x_label.get('value')) + title = x_label.get('label', self._format(pos)) + elif is_str(x_label): + pos = x_pos[i] + title = x_label + else: + pos = float(x_label) + title = self._x_format(x_label) + + self._x_labels.append((title, pos)) + self._box.xmin = min(self._box.xmin, min(cut(self._x_labels, 1))) + self._box.xmax = max(self._box.xmax, max(cut(self._x_labels, 1))) + + else: + self._x_labels = list(zip(map(self._x_format, x_pos), x_pos)) diff --git a/pygal/graph/funnel.py b/pygal/graph/funnel.py index 387b64a..98135b3 100644 --- a/pygal/graph/funnel.py +++ b/pygal/graph/funnel.py @@ -54,21 +54,25 @@ class Funnel(Graph): class_='funnel reactive tooltip-trigger'), metadata) x, y = self.view(( - self._x_labels[serie.index][1], # Poly center from label + # Poly center from label + self._center(self._x_pos[serie.index]), sum([point[1] for point in poly]) / len(poly))) self._tooltip_data(funnels, value, x, y, classes='centered') self._static_value(serie_node, value, x, y) + def _center(self, x): + return x - 1 / (2 * self._order) + def _compute(self): """Compute y min and max and y scale and set labels""" - x_pos = [ + self._x_pos = [ (x + 1) / self._order for x in range(self._order) ] if self._order != 1 else [.5] # Center if only one value previous = [[self.zero, self.zero] for i in range(self._len)] for i, serie in enumerate(self.series): y_height = - sum(serie.safe_values) / 2 - all_x_pos = [0] + x_pos + all_x_pos = [0] + self._x_pos serie.points = [] for j, value in enumerate(serie.values): poly = [] @@ -84,15 +88,13 @@ class Funnel(Graph): self._box.ymin = -val_max self._box.ymax = val_max - y_pos = compute_scale( - self._box.ymin, self._box.ymax, self.logarithmic, self.order_min, - self.min_scale, self.max_scale - ) if not self.y_labels else list(map(float, self.y_labels)) - + def _compute_x_labels(self): self._x_labels = list( - zip(cut(self.series, 'title'), - map(lambda x: x - 1 / (2 * self._order), x_pos))) - self._y_labels = list(zip(map(self._format, y_pos), y_pos)) + zip(self.x_labels or [ + 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): """Plot the funnel""" diff --git a/pygal/graph/gauge.py b/pygal/graph/gauge.py index a52f582..b6f1044 100644 --- a/pygal/graph/gauge.py +++ b/pygal/graph/gauge.py @@ -20,7 +20,8 @@ """Gauge chart representing values as needles on a polar scale""" from __future__ import division -from pygal.util import decorate, compute_scale, alter +from pygal._compat import is_str +from pygal.util import decorate, compute_scale, alter, cut from pygal.view import PolarThetaView, PolarThetaLogView from pygal.graph.graph import Graph @@ -90,7 +91,7 @@ class Gauge(Graph): def _y_axis(self, draw_axes=True): """Override y axis to plot a polar axis""" - axis = self.svg.node(self.nodes['plot'], class_="axis y x gauge") + axis = self.svg.node(self.nodes['plot'], class_="axis x gauge") for i, (label, theta) in enumerate(self._y_labels): guides = self.svg.node(axis, class_='guides') @@ -114,9 +115,13 @@ class Gauge(Graph): y=y ).text = label + self.svg.node( + guides, 'title', + ).text = self._format(theta) + def _x_axis(self, draw_axes=True): """Override x axis to put a center circle in center""" - axis = self.svg.node(self.nodes['plot'], class_="axis x gauge") + axis = self.svg.node(self.nodes['plot'], class_="axis y gauge") x, y = self.view((0, 0)) self.svg.node(axis, 'circle', cx=x, cy=y, r=4) @@ -132,12 +137,33 @@ class Gauge(Graph): 0, 1, self.min_, self.max_) - y_pos = compute_scale( - self.min_, self.max_, self.logarithmic, self.order_min, - self.min_scale, self.max_scale - ) if not self.y_labels else list(map(float, self.y_labels)) - self._y_labels = list(zip(map(self._format, y_pos), y_pos)) + def _compute_y_labels(self): + y_pos = compute_scale( + self.min_, self.max_, self.logarithmic, + self.order_min, self.min_scale, self.max_scale + ) + if self.y_labels: + self._y_labels = [] + for i, y_label in enumerate(self.y_labels): + if isinstance(y_label, dict): + pos = float(y_label.get('value')) + title = y_label.get('label', self._format(pos)) + elif is_str(y_label): + pos = y_pos[i] + title = y_label + else: + pos = float(y_label) + title = self._format(y_label) + self._y_labels.append((title, 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( + 0, 1, + self.min_, + self.max_) + else: + self._y_labels = list(zip(map(self._format, y_pos), y_pos)) def _plot(self): """Plot all needles""" diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index bda4868..b11ec97 100644 --- a/pygal/graph/graph.py +++ b/pygal/graph/graph.py @@ -19,12 +19,12 @@ """Chart properties and drawing""" from __future__ import division -from pygal._compat import is_list_like +from pygal._compat import is_list_like, is_str from pygal.interpolate import INTERPOLATIONS from pygal.graph.public import PublicApi from pygal.view import View, LogView, XYLogView, ReverseView from pygal.util import ( - cached_property, majorize, humanize, split_title, + cached_property, majorize, humanize, split_title, compute_scale, truncate, reverse_text_len, get_text_box, get_texts_box, cut, rad, decorate) from math import sqrt, ceil, cos, sin @@ -165,12 +165,10 @@ class Graph(PublicApi): class_='major' if major else '' ) - if isinstance(label, dict): - label = label['title'] - text.text = truncate(label, truncation) if text.text != label: self.svg.node(guides, 'title').text = label + if self.x_label_rotation: text.attrib['transform'] = "rotate(%d %f %f)" % ( self.x_label_rotation, x, y) @@ -242,14 +240,16 @@ class Graph(PublicApi): class_='major' if major else '' ) - if isinstance(label, dict): - label = label['title'] 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(position) + if self._y_2nd_labels: secondary_ax = self.svg.node( self.nodes['plot'], class_="axis y2") @@ -526,7 +526,9 @@ class Graph(PublicApi): if self.show_legend and series_group: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_legend or 15), - cut(series_group, 'title')), + [serie.title['title'] + if isinstance(serie.title, dict) + else serie.title for serie in series_group]), self.style.legend_font_size) if self.legend_at_bottom: h_max = max(h, self.legend_box_size) @@ -713,9 +715,38 @@ class Graph(PublicApi): cut(self._y_labels, 1) ) + def _compute_x_labels(self): + self._x_labels = self.x_labels and list(zip(self.x_labels, [ + (i + .5) / self._len for i in range(self._len)])) + + def _compute_y_labels(self): + y_pos = compute_scale( + self._box.ymin, self._box.ymax, self.logarithmic, + self.order_min, self.min_scale, self.max_scale + ) + if self.y_labels: + self._y_labels = [] + for i, y_label in enumerate(self.y_labels): + if isinstance(y_label, dict): + pos = float(y_label.get('value')) + title = y_label.get('label', self._format(pos)) + elif is_str(y_label): + pos = y_pos[i] + title = y_label + else: + pos = float(y_label) + title = self._format(y_label) + self._y_labels.append((title, 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: + self._y_labels = list(zip(map(self._format, y_pos), y_pos)) + def _draw(self): """Draw all the things""" self._compute() + self._compute_x_labels() + self._compute_y_labels() self._compute_secondary() self._post_compute() self._compute_margin() diff --git a/pygal/graph/histogram.py b/pygal/graph/histogram.py index 3d47bcd..4933674 100644 --- a/pygal/graph/histogram.py +++ b/pygal/graph/histogram.py @@ -23,16 +23,15 @@ as bars of varying width. from __future__ import division from pygal._compat import is_list_like -from pygal.graph.graph import Graph +from pygal.graph.dual import Dual from pygal.util import ( swap, ident, compute_scale, decorate, cached_property, alter) -class Histogram(Graph): +class Histogram(Dual): """Histogram chart class""" - _dual = True _series_margin = 0 @cached_property @@ -125,18 +124,6 @@ class Histogram(Graph): if yrng: self._box.ymin, self._box.ymax = ymin, ymax - x_pos = compute_scale( - self._box.xmin, self._box.xmax, self.logarithmic, self.order_min, - self.min_scale, self.max_scale - ) if not self.x_labels else list(map(float, self.x_labels)) - y_pos = compute_scale( - self._box.ymin, self._box.ymax, self.logarithmic, self.order_min, - self.min_scale, self.max_scale - ) if not self.y_labels else list(map(float, self.y_labels)) - - self._x_labels = list(zip(map(self._format, x_pos), x_pos)) - self._y_labels = list(zip(map(self._format, y_pos), y_pos)) - def _plot(self): """Draw bars for series and secondary series""" for serie in self.series: diff --git a/pygal/graph/line.py b/pygal/graph/line.py index ba952de..ea12665 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -24,7 +24,7 @@ connected by straight segments from __future__ import division from pygal.graph.graph import Graph -from pygal.util import cached_property, compute_scale, decorate, alter +from pygal.util import cached_property, compute_scale, decorate, alter, cut class Line(Graph): @@ -146,18 +146,6 @@ class Line(Graph): self._points(x_pos) - if self.x_labels: - label_len = len(self.x_labels) - if label_len != self._len: - label_pos = [0.5] if label_len == 1 else [ - x / (label_len - 1) for x in range(label_len) - ] - self._x_labels = list(zip(self.x_labels, label_pos)) - else: - self._x_labels = list(zip(self.x_labels, x_pos)) - else: - self._x_labels = None - if self.include_x_axis: # Y Label self._box.ymin = min(self._min or 0, 0) @@ -166,17 +154,6 @@ class Line(Graph): self._box.ymin = self._min self._box.ymax = self._max - if self.y_labels: - self._box.ymin = min(self._box.ymin, min(self.y_labels)) - self._box.ymax = max(self._box.ymax, max(self.y_labels)) - - y_pos = compute_scale( - self._box.ymin, self._box.ymax, self.logarithmic, self.order_min, - self.min_scale, self.max_scale - ) if not self.y_labels else list(map(float, self.y_labels)) - - self._y_labels = list(zip(map(self._format, y_pos), y_pos)) - def _plot(self): """Plot the serie lines and secondary serie lines""" for serie in self.series: diff --git a/pygal/graph/map.py b/pygal/graph/map.py index 19b40eb..3add2a0 100644 --- a/pygal/graph/map.py +++ b/pygal/graph/map.py @@ -117,3 +117,9 @@ class BaseMap(Graph): self.area_names[area_code], self._format(value)) self.nodes['plot'].append(map) + + def _compute_x_labels(self): + pass + + def _compute_y_labels(self): + pass diff --git a/pygal/graph/pie.py b/pygal/graph/pie.py index 3aff1a3..cc11336 100644 --- a/pygal/graph/pie.py +++ b/pygal/graph/pie.py @@ -94,6 +94,12 @@ class Pie(Graph): original_start_angle, center, val) return serie_angle + def _compute_x_labels(self): + pass + + def _compute_y_labels(self): + pass + def _plot(self): """Draw all the serie slices""" total = sum(map(sum, map(lambda x: x.values, self.series))) diff --git a/pygal/graph/radar.py b/pygal/graph/radar.py index d135366..040bbfa 100644 --- a/pygal/graph/radar.py +++ b/pygal/graph/radar.py @@ -27,6 +27,7 @@ from pygal.graph.line import Line from pygal.adapters import positive, none_to_zero from pygal.view import PolarView, PolarLogView from pygal.util import deg, cached_property, compute_scale, majorize, cut +from pygal._compat import is_str from math import cos, pi @@ -38,7 +39,7 @@ class Radar(Line): def __init__(self, *args, **kwargs): """Init custom vars""" - self.x_pos = None + self._x_pos = None self._rmax = None super(Radar, self).__init__(*args, **kwargs) @@ -152,11 +153,11 @@ class Radar(Line): continue guides = self.svg.node(axis, class_='guides') self.svg.line( - guides, [self.view((r, theta)) for theta in self.x_pos], + 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])) + x, y = self.view((r, self._x_pos[0])) self.svg.node( guides, 'text', x=x - 5, @@ -188,14 +189,34 @@ class Radar(Line): self._rmin = self.zero self._rmax = self._max or 1 self._box.set_polar_box(self._rmin, self._rmax) + self._self_close = True + self._x_pos = x_pos + + def _compute_x_labels(self): + self._x_labels = self.x_labels and list( + zip(self.x_labels, self._x_pos)) + def _compute_y_labels(self): y_pos = compute_scale( self._rmin, self._rmax, self.logarithmic, self.order_min, self.min_scale, self.max_scale / 2 - ) if not self.y_labels else list(map(int, self.y_labels)) - - self._x_labels = self.x_labels and list(zip(self.x_labels, x_pos)) - self._y_labels = list(zip(map(self._format, y_pos), y_pos)) + ) + if self.y_labels: + self._y_labels = [] + for i, y_label in enumerate(self.y_labels): + if isinstance(y_label, dict): + pos = float(y_label.get('value')) + title = y_label.get('label', self._format(pos)) + elif is_str(y_label): + pos = y_pos[i] + title = y_label + else: + pos = float(y_label) + title = self._format(y_label) + self._y_labels.append((title, 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) - self.x_pos = x_pos - self._self_close = True + else: + self._y_labels = list(zip(map(self._format, y_pos), y_pos)) diff --git a/pygal/graph/stackedbar.py b/pygal/graph/stackedbar.py index 90ec28b..aca0b57 100644 --- a/pygal/graph/stackedbar.py +++ b/pygal/graph/stackedbar.py @@ -70,15 +70,6 @@ class StackedBar(Bar): ] if self._len > 1 else [0, 1] # Center if only one value self._points(x_pos) - y_pos = compute_scale( - self._box.ymin, self._box.ymax, self.logarithmic, self.order_min, - self.min_scale, self.max_scale - ) if not self.y_labels else list(map(float, self.y_labels)) - self._x_ranges = zip(x_pos, x_pos[1:]) - - self._x_labels = self.x_labels and list(zip(self.x_labels, [ - sum(x_range) / 2 for x_range in self._x_ranges])) - self._y_labels = list(zip(map(self._format, y_pos), y_pos)) self.negative_cumulation = [0] * self._len self.positive_cumulation = [0] * self._len diff --git a/pygal/graph/xy.py b/pygal/graph/xy.py index 4e140c6..bd97ead 100644 --- a/pygal/graph/xy.py +++ b/pygal/graph/xy.py @@ -24,15 +24,15 @@ straight segments. from __future__ import division from functools import reduce -from pygal.util import compute_scale, cached_property, compose, ident +from pygal.util import compute_scale, cached_property, compose, ident, cut from pygal.graph.line import Line +from pygal.graph.dual import Dual -class XY(Line): +class XY(Line, Dual): """XY Line graph class""" - _dual = True _x_adapters = [] def _get_value(self, values, i): @@ -123,26 +123,5 @@ class XY(Line): if xrng: self._box.xmin, self._box.xmax = xmin, xmax - if self.x_labels: - self._box.xmin = min(self.x_labels) - self._box.xmax = max(self.x_labels) - if yrng: self._box.ymin, self._box.ymax = ymin, ymax - - if self.y_labels: - self._box.ymin = min(self.y_labels) - self._box.ymax = max(self.y_labels) - - x_pos = compute_scale( - self._box.xmin, self._box.xmax, self.logarithmic, self.order_min, - self.min_scale, self.max_scale - ) if not self.x_labels else list(map(float, self.x_labels)) - - y_pos = compute_scale( - self._box.ymin, self._box.ymax, self.logarithmic, self.order_min, - self.min_scale, self.max_scale - ) if not self.y_labels else list(map(float, self.y_labels)) - - self._x_labels = list(zip(map(self._x_format, x_pos), x_pos)) - self._y_labels = list(zip(map(self._format, y_pos), y_pos)) diff --git a/pygal/test/test_line.py b/pygal/test/test_line.py index 466e0fb..c1d44c4 100644 --- a/pygal/test/test_line.py +++ b/pygal/test/test_line.py @@ -95,6 +95,7 @@ def test_not_equal_x_labels(): """Test x_labels""" line = Line() line.add('test1', range(100)) + line.truncate_label = -1 line.x_labels = map(str, range(11)) q = line.render_pyquery() assert len(q(".dots")) == 100 diff --git a/pygal/util.py b/pygal/util.py index 52680b4..8a1d86a 100644 --- a/pygal/util.py +++ b/pygal/util.py @@ -24,7 +24,6 @@ from pygal._compat import u, is_list_like, to_unicode import re from decimal import Decimal from math import floor, pi, log, log10, ceil -from itertools import cycle ORDERS = u("yzafpnµm kMGTPEZY") @@ -215,13 +214,7 @@ def get_text_box(text, fs): def get_texts_box(texts, fs): """Approximation of multiple texts bounds""" - def get_text_title(texts): - for text in texts: - if isinstance(text, dict): - yield text['title'] - else: - yield text - max_len = max(map(len, get_text_title(texts))) + max_len = max(map(len, texts)) return (fs, text_len(max_len, fs))