Browse Source

Add histogram chart type. Fixes #48

pull/58/merge
Florian Mounier 11 years ago
parent
commit
5f2a78795e
  1. 14
      demo/moulinrouge/tests.py
  2. 3
      pygal/graph/__init__.py
  3. 148
      pygal/graph/histogram.py
  4. 7
      pygal/graph/xy.py
  5. 10
      pygal/util.py

14
demo/moulinrouge/tests.py

@ -2,7 +2,7 @@
# This file is part of pygal
from pygal import (
Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, XY,
CHARTS_BY_NAME, Config, Line, DateY, Worldmap)
CHARTS_BY_NAME, Config, Line, DateY, Worldmap, Histogram)
from pygal.style import styles
@ -217,6 +217,18 @@ def get_test_routes(app):
bar.add('2', [4, 5, 6])
return bar.render_response()
@app.route('/test/histogram')
def test_histogram():
hist = Histogram(style=styles['neon'])
hist.add('1', [
(2, 0, 1),
(4, 1, 3),
(3, 3.5, 5),
(1.5, 5, 10)
])
hist.add('2', [(2, 2, 8)])
return hist.render_response()
@app.route('/test/secondary/<chart>')
def test_secondary_for(chart):
chart = CHARTS_BY_NAME[chart](fill=True)

3
pygal/graph/__init__.py

@ -37,5 +37,6 @@ CHARTS_NAMES = [
'Dot',
'Gauge',
'DateY',
'Worldmap'
'Worldmap',
'Histogram'
]

148
pygal/graph/histogram.py

@ -0,0 +1,148 @@
# -*- coding: utf-8 -*-
# This file is part of pygal
#
# A python svg graph plotting library
# Copyright © 2012-2013 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/>.
"""
Histogram chart
"""
from __future__ import division
from pygal.graph.graph import Graph
from pygal.util import swap, ident, compute_scale, decorate, cached_property
class Histogram(Graph):
"""Histogram chart"""
_dual = True
_series_margin = 0
_serie_margin = 0
@cached_property
def _values(self):
"""Getter for secondary series values (flattened)"""
return [val[0]
for serie in self.series
for val in serie.values
if val[0] is not None]
@cached_property
def _secondary_values(self):
"""Getter for secondary series values (flattened)"""
return [val[0]
for serie in self.secondary_series
for val in serie.values
if val[0] is not None]
@cached_property
def xvals(self):
return [val
for serie in self.all_series
for dval in serie.values
for val in dval[1:3]
if val is not None]
@cached_property
def yvals(self):
return [val[0]
for serie in self.series
for val in serie.values
if val[0] is not None]
def _has_data(self):
"""Check if there is any data"""
return sum(
map(len, map(lambda s: s.safe_values, self.series))) != 0 and any((
sum(map(abs, self.xvals)) != 0,
sum(map(abs, self.yvals)) != 0))
def _bar(self, parent, x0, x1, y, index, i, zero, secondary=False):
x, y = self.view((x0, y))
x1, _ = self.view((x1, y))
width = x1 - x
height = self.view.y(zero) - y
series_margin = width * self._series_margin
x += series_margin
width -= 2 * series_margin
r = self.rounded_bars * 1 if self.rounded_bars else 0
self.svg.transposable_node(
parent, 'rect',
x=x, y=y, rx=r, ry=r, width=width, height=height,
class_='rect reactive tooltip-trigger')
transpose = swap if self.horizontal else ident
return transpose((x + width / 2, y + height / 2))
def bar(self, serie_node, serie, index, rescale=False):
"""Draw a bar graph for a serie"""
bars = self.svg.node(serie_node['plot'], class_="histbars")
points = serie.points
for i, (y, x0, x1) in enumerate(points):
if None in (x0, x1, y) or (self.logarithmic and y <= 0):
continue
metadata = serie.metadata.get(i)
bar = decorate(
self.svg,
self.svg.node(bars, class_='histbar'),
metadata)
val = self._format(serie.values[i][0])
x_center, y_center = self._bar(
bar, x0, x1, y, index, i, self.zero, secondary=rescale)
self._tooltip_data(
bar, val, x_center, y_center, classes="centered")
self._static_value(serie_node, val, x_center, y_center)
def _compute(self):
if self.xvals:
xmin = min(self.xvals)
xmax = max(self.xvals)
xrng = (xmax - xmin)
else:
xrng = None
if self.yvals:
ymin = min(min(self.yvals), self.zero)
ymax = max(max(self.yvals), self.zero)
yrng = (ymax - ymin)
else:
yrng = None
for serie in self.all_series:
serie.points = serie.values
if xrng:
self._box.xmin, self._box.xmax = xmin, xmax
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)
y_pos = compute_scale(
self._box.ymin, self._box.ymax, self.logarithmic, self.order_min)
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):
for index, serie in enumerate(self.series):
self.bar(self._serie(index), serie, index)
for index, serie in enumerate(self.secondary_series, len(self.series)):
self.bar(self._serie(index), serie, index, True)

7
pygal/graph/xy.py

@ -51,9 +51,6 @@ class XY(Line):
sum(map(abs, self.xvals)) != 0,
sum(map(abs, self.yvals)) != 0))
def _get_value(self, values, i):
return 'x=%s, y=%s' % tuple(map(self._format, values[i]))
def _compute(self):
if self.xvals:
xmin = min(self.xvals)
@ -92,9 +89,9 @@ class XY(Line):
xrng = None
if xrng:
self._box.xmin, self._box.xmax = min(self.xvals), max(self.xvals)
self._box.xmin, self._box.xmax = xmin, xmax
if yrng:
self._box.ymin, self._box.ymax = min(self.yvals), max(self.yvals)
self._box.ymin, self._box.ymax = ymin, ymax
x_pos = compute_scale(
self._box.xmin, self._box.xmax, self.logarithmic, self.order_min)

10
pygal/util.py

@ -298,6 +298,7 @@ from pygal.serie import Serie
def prepare_values(raw, config, cls):
"""Prepare the values to start with sane values"""
from pygal.graph.datey import DateY
from pygal.graph.histogram import Histogram
from pygal.graph.worldmap import Worldmap
if config.x_labels is None and hasattr(cls, 'x_labels'):
config.x_labels = cls.x_labels
@ -350,7 +351,14 @@ def prepare_values(raw, config, cls):
else:
value = raw_value
if cls._dual:
# Fix this by doing this in charts class methods
if issubclass(cls, Histogram):
if value is None:
value = (None, None, None)
elif not hasattr(value, '__iter__'):
value = (value, config.zero, config.zero)
value = list(map(adapter, value))
elif cls._dual:
if value is None:
value = (None, None)
elif not hasattr(value, '__iter__'):

Loading…
Cancel
Save