Browse Source

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)

pull/264/head
Florian Mounier 10 years ago
parent
commit
7748f847f3
  1. 4
      demo/moulinrouge/templates/svgs.jinja2
  2. 47
      demo/moulinrouge/tests.py
  3. 7
      docs/api/pygal.graph.dual.rst
  4. 1
      docs/api/pygal.graph.rst
  5. 1
      docs/changelog.rst
  6. 60
      docs/documentation/configuration/value.rst
  7. 11
      pygal/graph/bar.py
  8. 13
      pygal/graph/box.py
  9. 18
      pygal/graph/dot.py
  10. 53
      pygal/graph/dual.py
  11. 24
      pygal/graph/funnel.py
  12. 40
      pygal/graph/gauge.py
  13. 47
      pygal/graph/graph.py
  14. 17
      pygal/graph/histogram.py
  15. 25
      pygal/graph/line.py
  16. 6
      pygal/graph/map.py
  17. 6
      pygal/graph/pie.py
  18. 37
      pygal/graph/radar.py
  19. 9
      pygal/graph/stackedbar.py
  20. 27
      pygal/graph/xy.py
  21. 1
      pygal/test/test_line.py
  22. 9
      pygal/util.py

4
demo/moulinrouge/templates/svgs.jinja2

@ -1,10 +1,10 @@
{% extends '_layout.jinja2' %} {% extends '_layout.jinja2' %}
{% block section %} {% block section %}
{% for svg in svgs %} {% for svg in svgs | sort(attribute='type') %}
<figure> <figure>
<embed src="{{ url_for('svg', type=svg.type, series=svg.series, config=svg.config) }}" type="image/svg+xml" width="{{ width }}" height="{{ height }}" /> <embed src="{{ url_for('svg', type=svg.type, series=svg.series, config=svg.config) }}" type="image/svg+xml" width="{{ width }}" height="{{ height }}" />
<figcaption></figcaption> <figcaption>{{ svg.type }}</figcaption>
</figure> </figure>
{% endfor %} {% endfor %}
{% endblock section %} {% endblock section %}

47
demo/moulinrouge/tests.py

@ -180,6 +180,12 @@ def get_test_routes(app):
gauge.add('Need m', [-4]) gauge.add('Need m', [-4])
gauge.add('Need z', [-10, 10.5]) gauge.add('Need z', [-10, 10.5])
gauge.add('No', [99, -99]) gauge.add('No', [99, -99])
gauge.y_labels = [
{'label': 'X',
'value': 6},
{'label': '><',
'value': -6}
]
return gauge.render_response() return gauge.render_response()
@app.route('/test/gauge/log') @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('2', [7, -4, 10, None, 8, 3, 1])
graph.add('3', [7, -14, -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.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 graph.x_label_rotation = 90
return graph.render_response() return graph.render_response()
@ -341,13 +347,24 @@ def get_test_routes(app):
(1.5, 5, 10) (1.5, 5, 10)
]) ])
hist.add('2', [(2, 2, 8)]) hist.add('2', [(2, 2, 8)])
hist.x_labels = [0, 3, 6, 9, 12]
return hist.render_response() return hist.render_response()
@app.route('/test/ylabels') @app.route('/test/ylabels')
def test_ylabels(): def test_ylabels():
chart = Line() chart = Bar()
chart.x_labels = 'Red', 'Blue', 'Green' 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]) chart.add('line', [.0002, .0005, .00035])
return chart.render_response() return chart.render_response()
@ -398,6 +415,7 @@ def get_test_routes(app):
stacked = StackedBar(stack_from_top=True) stacked = StackedBar(stack_from_top=True)
stacked.add('1', [1, 2, 3]) stacked.add('1', [1, 2, 3])
stacked.add('2', [4, 5, 6]) stacked.add('2', [4, 5, 6])
stacked.x_labels = ['a', 'b', 'c']
return stacked.render_response() return stacked.render_response()
@app.route('/test/show_dots') @app.route('/test/show_dots')
@ -603,9 +621,10 @@ def get_test_routes(app):
@app.route('/test/x_major_labels/<chart>') @app.route('/test/x_major_labels/<chart>')
def test_x_major_labels_for(chart): def test_x_major_labels_for(chart):
chart = CHARTS_BY_NAME[chart]() chart = CHARTS_BY_NAME[chart]()
for i in range(12):
chart.add('test', range(12)) chart.add('test', range(12))
chart.x_labels = map(str, range(12)) chart.x_labels = map(str, range(10))
chart.x_labels_major_count = 4 # chart.x_labels_major_count = 4
# chart.x_labels_major = ['1', '5', '11', '1.0', '5.0', '11.0'] # chart.x_labels_major = ['1', '5', '11', '1.0', '5.0', '11.0']
return chart.render_response() return chart.render_response()
@ -794,4 +813,22 @@ def get_test_routes(app):
line.add('_', [1, 32, 12, .4, .009]) line.add('_', [1, 32, 12, .4, .009])
return line.render_response() return line.render_response()
@app.route('/test/legendlink/<chart>')
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()))) return list(sorted(filter(lambda x: x.startswith('test'), locals())))

7
docs/api/pygal.graph.dual.rst

@ -0,0 +1,7 @@
pygal.graph.dual module
=======================
.. automodule:: pygal.graph.dual
:members:
:undoc-members:
:show-inheritance:

1
docs/api/pygal.graph.rst

@ -15,6 +15,7 @@ Submodules
pygal.graph.base pygal.graph.base
pygal.graph.box pygal.graph.box
pygal.graph.dot pygal.graph.dot
pygal.graph.dual
pygal.graph.funnel pygal.graph.funnel
pygal.graph.gauge pygal.graph.gauge
pygal.graph.graph pygal.graph.graph

1
docs/changelog.rst

@ -37,6 +37,7 @@ Changelog
* Add ``tooltip_fancy_mode`` to revert to old tooltips * Add ``tooltip_fancy_mode`` to revert to old tooltips
* Add auto ``print_value`` color + a configurable ``value_colors`` list in style * 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) * 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 1.7.0
===== =====

60
docs/documentation/configuration/value.rst

@ -142,3 +142,63 @@ You can specify a dictionary to xlink with all links attributes:
'target': '_self'} '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'}
}])

11
pygal/graph/bar.py

@ -24,7 +24,7 @@ proportional to the values that they represent.
from __future__ import division from __future__ import division
from pygal.graph.graph import Graph 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): class Bar(Graph):
@ -104,15 +104,6 @@ class Bar(Graph):
self._points(x_pos) 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): def _compute_secondary(self):
"""Compute parameters for secondary series rendering""" """Compute parameters for secondary series rendering"""
if self.secondary_series: if self.secondary_series:

13
pygal/graph/box.py

@ -76,20 +76,9 @@ class Box(Graph):
if self._max: if self._max:
self._box.ymax = max(self._max, self.zero) self._box.ymax = max(self._max, self.zero)
x_pos = [ def _compute_x_labels(self):
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))
self._x_labels = self.x_labels and list(zip(self.x_labels, [ self._x_labels = self.x_labels and list(zip(self.x_labels, [
(i + .5) / self._order for i in range(self._order)])) (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): def _plot(self):
"""Plot the series data""" """Plot the series data"""

18
pygal/graph/dot.py

@ -76,17 +76,25 @@ class Dot(Graph):
self._box.xmax = x_len self._box.xmax = x_len
self._box.ymax = y_len self._box.ymax = y_len
x_pos = [n / 2 for n in range(1, 2 * x_len, 2)] self._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._y_pos = [n / 2 for n in reversed(range(1, 2 * y_len, 2))]
for j, serie in enumerate(self.series): for j, serie in enumerate(self.series):
serie.points = [ serie.points = [
(x_pos[i], y_pos[j]) (self._x_pos[i], self._y_pos[j])
for i in range(x_len)] 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 = 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): def _set_view(self):
"""Assign a view to current graph""" """Assign a view to current graph"""

53
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 <http://www.gnu.org/licenses/>.
"""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))

24
pygal/graph/funnel.py

@ -54,21 +54,25 @@ class Funnel(Graph):
class_='funnel reactive tooltip-trigger'), metadata) class_='funnel reactive tooltip-trigger'), metadata)
x, y = self.view(( 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))) sum([point[1] for point in poly]) / len(poly)))
self._tooltip_data(funnels, value, x, y, classes='centered') self._tooltip_data(funnels, value, x, y, classes='centered')
self._static_value(serie_node, value, x, y) self._static_value(serie_node, value, x, y)
def _center(self, x):
return x - 1 / (2 * self._order)
def _compute(self): def _compute(self):
"""Compute y min and max and y scale and set labels""" """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) (x + 1) / self._order for x in range(self._order)
] if self._order != 1 else [.5] # Center if only one value ] if self._order != 1 else [.5] # Center if only one value
previous = [[self.zero, self.zero] for i in range(self._len)] previous = [[self.zero, self.zero] for i in range(self._len)]
for i, serie in enumerate(self.series): for i, serie in enumerate(self.series):
y_height = - sum(serie.safe_values) / 2 y_height = - sum(serie.safe_values) / 2
all_x_pos = [0] + x_pos all_x_pos = [0] + self._x_pos
serie.points = [] serie.points = []
for j, value in enumerate(serie.values): for j, value in enumerate(serie.values):
poly = [] poly = []
@ -84,15 +88,13 @@ class Funnel(Graph):
self._box.ymin = -val_max self._box.ymin = -val_max
self._box.ymax = val_max self._box.ymax = val_max
y_pos = compute_scale( def _compute_x_labels(self):
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( self._x_labels = list(
zip(cut(self.series, 'title'), zip(self.x_labels or [
map(lambda x: x - 1 / (2 * self._order), x_pos))) serie.title['title']
self._y_labels = list(zip(map(self._format, y_pos), y_pos)) if isinstance(serie.title, dict)
else serie.title for serie in self.series],
map(self._center, self._x_pos)))
def _plot(self): def _plot(self):
"""Plot the funnel""" """Plot the funnel"""

40
pygal/graph/gauge.py

@ -20,7 +20,8 @@
"""Gauge chart representing values as needles on a polar scale""" """Gauge chart representing values as needles on a polar scale"""
from __future__ import division 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.view import PolarThetaView, PolarThetaLogView
from pygal.graph.graph import Graph from pygal.graph.graph import Graph
@ -90,7 +91,7 @@ class Gauge(Graph):
def _y_axis(self, draw_axes=True): def _y_axis(self, draw_axes=True):
"""Override y axis to plot a polar axis""" """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): for i, (label, theta) in enumerate(self._y_labels):
guides = self.svg.node(axis, class_='guides') guides = self.svg.node(axis, class_='guides')
@ -114,9 +115,13 @@ class Gauge(Graph):
y=y y=y
).text = label ).text = label
self.svg.node(
guides, 'title',
).text = self._format(theta)
def _x_axis(self, draw_axes=True): def _x_axis(self, draw_axes=True):
"""Override x axis to put a center circle in center""" """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)) x, y = self.view((0, 0))
self.svg.node(axis, 'circle', cx=x, cy=y, r=4) self.svg.node(axis, 'circle', cx=x, cy=y, r=4)
@ -132,11 +137,32 @@ class Gauge(Graph):
0, 1, 0, 1,
self.min_, self.min_,
self.max_) 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))
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)) self._y_labels = list(zip(map(self._format, y_pos), y_pos))
def _plot(self): def _plot(self):

47
pygal/graph/graph.py

@ -19,12 +19,12 @@
"""Chart properties and drawing""" """Chart properties and drawing"""
from __future__ import division 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.interpolate import INTERPOLATIONS
from pygal.graph.public import PublicApi from pygal.graph.public import PublicApi
from pygal.view import View, LogView, XYLogView, ReverseView from pygal.view import View, LogView, XYLogView, ReverseView
from pygal.util import ( 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, truncate, reverse_text_len, get_text_box, get_texts_box, cut, rad,
decorate) decorate)
from math import sqrt, ceil, cos, sin from math import sqrt, ceil, cos, sin
@ -165,12 +165,10 @@ class Graph(PublicApi):
class_='major' if major else '' class_='major' if major else ''
) )
if isinstance(label, dict):
label = label['title']
text.text = truncate(label, truncation) text.text = truncate(label, truncation)
if text.text != label: if text.text != label:
self.svg.node(guides, 'title').text = label self.svg.node(guides, 'title').text = label
if self.x_label_rotation: if self.x_label_rotation:
text.attrib['transform'] = "rotate(%d %f %f)" % ( text.attrib['transform'] = "rotate(%d %f %f)" % (
self.x_label_rotation, x, y) self.x_label_rotation, x, y)
@ -242,14 +240,16 @@ class Graph(PublicApi):
class_='major' if major else '' class_='major' if major else ''
) )
if isinstance(label, dict):
label = label['title']
text.text = label text.text = label
if self.y_label_rotation: if self.y_label_rotation:
text.attrib['transform'] = "rotate(%d %f %f)" % ( text.attrib['transform'] = "rotate(%d %f %f)" % (
self.y_label_rotation, x, y) self.y_label_rotation, x, y)
self.svg.node(
guides, 'title',
).text = self._format(position)
if self._y_2nd_labels: if self._y_2nd_labels:
secondary_ax = self.svg.node( secondary_ax = self.svg.node(
self.nodes['plot'], class_="axis y2") self.nodes['plot'], class_="axis y2")
@ -526,7 +526,9 @@ class Graph(PublicApi):
if self.show_legend and series_group: if self.show_legend and series_group:
h, w = get_texts_box( h, w = get_texts_box(
map(lambda x: truncate(x, self.truncate_legend or 15), 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) self.style.legend_font_size)
if self.legend_at_bottom: if self.legend_at_bottom:
h_max = max(h, self.legend_box_size) h_max = max(h, self.legend_box_size)
@ -713,9 +715,38 @@ class Graph(PublicApi):
cut(self._y_labels, 1) 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): def _draw(self):
"""Draw all the things""" """Draw all the things"""
self._compute() self._compute()
self._compute_x_labels()
self._compute_y_labels()
self._compute_secondary() self._compute_secondary()
self._post_compute() self._post_compute()
self._compute_margin() self._compute_margin()

17
pygal/graph/histogram.py

@ -23,16 +23,15 @@ as bars of varying width.
from __future__ import division from __future__ import division
from pygal._compat import is_list_like from pygal._compat import is_list_like
from pygal.graph.graph import Graph from pygal.graph.dual import Dual
from pygal.util import ( from pygal.util import (
swap, ident, compute_scale, decorate, cached_property, alter) swap, ident, compute_scale, decorate, cached_property, alter)
class Histogram(Graph): class Histogram(Dual):
"""Histogram chart class""" """Histogram chart class"""
_dual = True
_series_margin = 0 _series_margin = 0
@cached_property @cached_property
@ -125,18 +124,6 @@ class Histogram(Graph):
if yrng: if yrng:
self._box.ymin, self._box.ymax = ymin, ymax 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): def _plot(self):
"""Draw bars for series and secondary series""" """Draw bars for series and secondary series"""
for serie in self.series: for serie in self.series:

25
pygal/graph/line.py

@ -24,7 +24,7 @@ connected by straight segments
from __future__ import division from __future__ import division
from pygal.graph.graph import Graph 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): class Line(Graph):
@ -146,18 +146,6 @@ class Line(Graph):
self._points(x_pos) 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: if self.include_x_axis:
# Y Label # Y Label
self._box.ymin = min(self._min or 0, 0) self._box.ymin = min(self._min or 0, 0)
@ -166,17 +154,6 @@ class Line(Graph):
self._box.ymin = self._min self._box.ymin = self._min
self._box.ymax = self._max 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): def _plot(self):
"""Plot the serie lines and secondary serie lines""" """Plot the serie lines and secondary serie lines"""
for serie in self.series: for serie in self.series:

6
pygal/graph/map.py

@ -117,3 +117,9 @@ class BaseMap(Graph):
self.area_names[area_code], self._format(value)) self.area_names[area_code], self._format(value))
self.nodes['plot'].append(map) self.nodes['plot'].append(map)
def _compute_x_labels(self):
pass
def _compute_y_labels(self):
pass

6
pygal/graph/pie.py

@ -94,6 +94,12 @@ class Pie(Graph):
original_start_angle, center, val) original_start_angle, center, val)
return serie_angle return serie_angle
def _compute_x_labels(self):
pass
def _compute_y_labels(self):
pass
def _plot(self): def _plot(self):
"""Draw all the serie slices""" """Draw all the serie slices"""
total = sum(map(sum, map(lambda x: x.values, self.series))) total = sum(map(sum, map(lambda x: x.values, self.series)))

37
pygal/graph/radar.py

@ -27,6 +27,7 @@ from pygal.graph.line import Line
from pygal.adapters import positive, none_to_zero from pygal.adapters import positive, none_to_zero
from pygal.view import PolarView, PolarLogView from pygal.view import PolarView, PolarLogView
from pygal.util import deg, cached_property, compute_scale, majorize, cut from pygal.util import deg, cached_property, compute_scale, majorize, cut
from pygal._compat import is_str
from math import cos, pi from math import cos, pi
@ -38,7 +39,7 @@ class Radar(Line):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Init custom vars""" """Init custom vars"""
self.x_pos = None self._x_pos = None
self._rmax = None self._rmax = None
super(Radar, self).__init__(*args, **kwargs) super(Radar, self).__init__(*args, **kwargs)
@ -152,11 +153,11 @@ class Radar(Line):
continue continue
guides = self.svg.node(axis, class_='guides') guides = self.svg.node(axis, class_='guides')
self.svg.line( 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, close=True,
class_='%sguide line' % ( class_='%sguide line' % (
'major ' if major else '')) '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( self.svg.node(
guides, 'text', guides, 'text',
x=x - 5, x=x - 5,
@ -188,14 +189,34 @@ class Radar(Line):
self._rmin = self.zero self._rmin = self.zero
self._rmax = self._max or 1 self._rmax = self._max or 1
self._box.set_polar_box(self._rmin, self._rmax) 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( y_pos = compute_scale(
self._rmin, self._rmax, self.logarithmic, self.order_min, self._rmin, self._rmax, self.logarithmic, self.order_min,
self.min_scale, self.max_scale / 2 self.min_scale, self.max_scale / 2
) if not self.y_labels else list(map(int, self.y_labels)) )
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_labels = self.x_labels and list(zip(self.x_labels, x_pos)) else:
self._y_labels = list(zip(map(self._format, y_pos), y_pos)) self._y_labels = list(zip(map(self._format, y_pos), y_pos))
self.x_pos = x_pos
self._self_close = True

9
pygal/graph/stackedbar.py

@ -70,15 +70,6 @@ class StackedBar(Bar):
] if self._len > 1 else [0, 1] # Center if only one value ] if self._len > 1 else [0, 1] # Center if only one value
self._points(x_pos) 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.negative_cumulation = [0] * self._len
self.positive_cumulation = [0] * self._len self.positive_cumulation = [0] * self._len

27
pygal/graph/xy.py

@ -24,15 +24,15 @@ straight segments.
from __future__ import division from __future__ import division
from functools import reduce 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.line import Line
from pygal.graph.dual import Dual
class XY(Line): class XY(Line, Dual):
"""XY Line graph class""" """XY Line graph class"""
_dual = True
_x_adapters = [] _x_adapters = []
def _get_value(self, values, i): def _get_value(self, values, i):
@ -123,26 +123,5 @@ class XY(Line):
if xrng: if xrng:
self._box.xmin, self._box.xmax = xmin, xmax 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: if yrng:
self._box.ymin, self._box.ymax = ymin, ymax 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))

1
pygal/test/test_line.py

@ -95,6 +95,7 @@ def test_not_equal_x_labels():
"""Test x_labels""" """Test x_labels"""
line = Line() line = Line()
line.add('test1', range(100)) line.add('test1', range(100))
line.truncate_label = -1
line.x_labels = map(str, range(11)) line.x_labels = map(str, range(11))
q = line.render_pyquery() q = line.render_pyquery()
assert len(q(".dots")) == 100 assert len(q(".dots")) == 100

9
pygal/util.py

@ -24,7 +24,6 @@ from pygal._compat import u, is_list_like, to_unicode
import re import re
from decimal import Decimal from decimal import Decimal
from math import floor, pi, log, log10, ceil from math import floor, pi, log, log10, ceil
from itertools import cycle
ORDERS = u("yzafpnµm kMGTPEZY") ORDERS = u("yzafpnµm kMGTPEZY")
@ -215,13 +214,7 @@ def get_text_box(text, fs):
def get_texts_box(texts, fs): def get_texts_box(texts, fs):
"""Approximation of multiple texts bounds""" """Approximation of multiple texts bounds"""
def get_text_title(texts): max_len = max(map(len, texts))
for text in texts:
if isinstance(text, dict):
yield text['title']
else:
yield text
max_len = max(map(len, get_text_title(texts)))
return (fs, text_len(max_len, fs)) return (fs, text_len(max_len, fs))

Loading…
Cancel
Save