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') %}
{% 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))