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 # This file is part of pygal
from pygal import ( from pygal import (
Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, XY, 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 from pygal.style import styles
@ -217,6 +217,18 @@ def get_test_routes(app):
bar.add('2', [4, 5, 6]) bar.add('2', [4, 5, 6])
return bar.render_response() 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>') @app.route('/test/secondary/<chart>')
def test_secondary_for(chart): def test_secondary_for(chart):
chart = CHARTS_BY_NAME[chart](fill=True) chart = CHARTS_BY_NAME[chart](fill=True)

3
pygal/graph/__init__.py

@ -37,5 +37,6 @@ CHARTS_NAMES = [
'Dot', 'Dot',
'Gauge', 'Gauge',
'DateY', '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.xvals)) != 0,
sum(map(abs, self.yvals)) != 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): def _compute(self):
if self.xvals: if self.xvals:
xmin = min(self.xvals) xmin = min(self.xvals)
@ -92,9 +89,9 @@ class XY(Line):
xrng = None xrng = None
if xrng: if xrng:
self._box.xmin, self._box.xmax = min(self.xvals), max(self.xvals) self._box.xmin, self._box.xmax = xmin, xmax
if yrng: 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( x_pos = compute_scale(
self._box.xmin, self._box.xmax, self.logarithmic, self.order_min) 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): def prepare_values(raw, config, cls):
"""Prepare the values to start with sane values""" """Prepare the values to start with sane values"""
from pygal.graph.datey import DateY from pygal.graph.datey import DateY
from pygal.graph.histogram import Histogram
from pygal.graph.worldmap import Worldmap from pygal.graph.worldmap import Worldmap
if config.x_labels is None and hasattr(cls, 'x_labels'): if config.x_labels is None and hasattr(cls, 'x_labels'):
config.x_labels = cls.x_labels config.x_labels = cls.x_labels
@ -350,7 +351,14 @@ def prepare_values(raw, config, cls):
else: else:
value = raw_value 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: if value is None:
value = (None, None) value = (None, None)
elif not hasattr(value, '__iter__'): elif not hasattr(value, '__iter__'):

Loading…
Cancel
Save