From 8453a94f7b8bd74044f12a04e9ba6abe55c79aa6 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 18 Apr 2012 16:59:12 +0200 Subject: [PATCH] Add funnel and pyramid --- demo/simple_test.py | 26 ++++++++++- pygal/__init__.py | 6 ++- pygal/graph/bar.py | 9 ++-- pygal/graph/base.py | 5 +++ pygal/graph/funnel.py | 97 ++++++++++++++++++++++++++++++++++++++++++ pygal/graph/line.py | 20 ++++----- pygal/graph/pyramid.py | 77 +++++++++++++++++++++++++++++++++ pygal/graph/radar.py | 11 +++-- pygal/view.py | 8 ++-- 9 files changed, 233 insertions(+), 26 deletions(-) create mode 100644 pygal/graph/funnel.py create mode 100644 pygal/graph/pyramid.py diff --git a/demo/simple_test.py b/demo/simple_test.py index c2b2813..038205a 100755 --- a/demo/simple_test.py +++ b/demo/simple_test.py @@ -17,13 +17,35 @@ # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . -import sys +import time from pygal import * from pygal.style import * from math import cos, sin lnk = lambda v, l=None: {'value': v, 'xlink': 'javascript:alert("Test %s")' % v, 'label': l} +t_start = time.time() + +pyramid = Pyramid() + +pyramid.x_labels = ['0-25', '25-45', '45-65', '65+'] +pyramid.add('Man single', [2, 4, 2, 1]) +pyramid.add('Woman single', [10, 6, 1, 1]) +pyramid.add('Man maried', [10, 3, 4, 2]) +pyramid.add('Woman maried', [3, 3, 5, 3]) + +pyramid.render_to_file('out-pyramid.svg') + + +funnel = Funnel() + +funnel.add('1', [1, 2, 3]) +funnel.add('3', [3, 4, 5]) +funnel.add('6', [6, 5, 4]) +funnel.add('12', [12, 2, 9]) + +funnel.render_to_file('out-funnel.svg') + dot = Dot() dot.x_labels = map(str, range(4)) @@ -170,3 +192,5 @@ radar.add('test2', [10, 2, 0, 5, 1, 9, 4]) radar.title = "Radar test" radar.render_to_file('out-radar.svg') + +print "Ok (%dms)" % (1000 * (time.time() - t_start)) diff --git a/pygal/__init__.py b/pygal/__init__.py index 4852c62..792b4b7 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -21,7 +21,7 @@ Pygal - A python svg graph plotting library """ -__version__ = '0.9.21' +__version__ = '0.9.22' from pygal.config import Config from pygal.graph.bar import Bar @@ -30,6 +30,8 @@ from pygal.graph.horizontal import HorizontalBar from pygal.graph.horizontal import HorizontalStackedBar from pygal.graph.line import Line from pygal.graph.pie import Pie +from pygal.graph.funnel import Funnel +from pygal.graph.pyramid import Pyramid from pygal.graph.radar import Radar from pygal.graph.stackedbar import StackedBar from pygal.graph.stackedline import StackedLine @@ -39,11 +41,13 @@ from pygal.graph.xy import XY #: List of all chart types CHARTS = [ Bar, + Funnel, Dot, HorizontalBar, HorizontalStackedBar, Line, Pie, + Pyramid, Radar, StackedBar, StackedLine, diff --git a/pygal/graph/bar.py b/pygal/graph/bar.py index 96be243..dae45b2 100644 --- a/pygal/graph/bar.py +++ b/pygal/graph/bar.py @@ -115,11 +115,10 @@ class Bar(Graph): return stack_vals def _compute(self): - self._box.ymin = min(min(self._values), self.zero) - self._box.ymax = max(max(self._values), self.zero) - x_step = len(self.series[0].values) - x_pos = [x / x_step for x in range(x_step + 1) - ] if x_step > 1 else [0, 1] # Center if only one value + self._box.ymin = min(self._min, self.zero) + 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 y_pos = compute_scale( self._box.ymin, self._box.ymax, self.logarithmic ) if not self.y_labels else map(float, self.y_labels) diff --git a/pygal/graph/base.py b/pygal/graph/base.py index 6ee0442..577192e 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -125,6 +125,11 @@ class BaseGraph(object): """Getter for the maximum series size""" return max([len(serie.values) for serie in self.series]) + @cached_property + def _min(self): + """Getter for the minimum series value""" + return min(self._values) + @cached_property def _max(self): """Getter for the maximum series value""" diff --git a/pygal/graph/funnel.py b/pygal/graph/funnel.py new file mode 100644 index 0000000..3067864 --- /dev/null +++ b/pygal/graph/funnel.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# This file is part of pygal +# +# A python svg graph plotting library +# Copyright © 2012 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 . +""" +Funnel chart + +""" + +from __future__ import division +from pygal.util import decorate, cut, compute_scale +from pygal.serie import PositiveValue +from pygal.graph.graph import Graph + + +class Funnel(Graph): + """Funnel graph""" + + __value__ = PositiveValue + + def _format(self, value): + return super(Funnel, self)._format(abs(value)) + + def funnel(self, serie_node, serie): + """Draw a dot line""" + + fmt = lambda x: '%f %f' % x + for i, poly in enumerate(serie.points): + metadata = serie.metadata[i] + value = self._format(serie.values[i]) + + funnels = decorate( + self.svg, + self.svg.node(serie_node['plot'], class_="funnels"), + metadata) + + self.svg.node( + funnels, 'polygon', + points=' '.join(map(fmt, map(self.view, poly))), + class_='funnel reactive tooltip-trigger') + + x, y = self.view(( + self._x_labels[serie.index][1], # Poly center from label + 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 _compute(self): + xlen = len(self.series) + x_pos = [(x + 1) / xlen for x in range(xlen) + ] if xlen != 1 else [.5] # Center if only one value + + previous = [[0, 0] for i in range(self._len)] + for i, serie in enumerate(self.series): + y_height = - sum(serie.values) / 2 + all_x_pos = [0] + x_pos + serie.points = [] + for j, value in enumerate(serie.values): + poly = [] + poly.append((all_x_pos[i], previous[j][0])) + poly.append((all_x_pos[i], previous[j][1])) + previous[j][0] = y_height + y_height = previous[j][1] = y_height + value + poly.append((all_x_pos[i + 1], previous[j][1])) + poly.append((all_x_pos[i + 1], previous[j][0])) + serie.points.append(poly) + + val_max = max(map(sum, cut(self.series, 'values'))) + self._box.ymin = -val_max + self._box.ymax = val_max + + y_pos = compute_scale( + self._box.ymin, self._box.ymax, self.logarithmic + ) if not self.y_labels else map(float, self.y_labels) + + self._x_labels = zip(cut(self.series, 'title'), + map(lambda x: x - 1 / (2 * xlen), x_pos)) + self._y_labels = zip(map(self._format, y_pos), y_pos) + + def _plot(self): + for serie in self.series: + self.funnel( + self._serie(serie.index), serie) diff --git a/pygal/graph/line.py b/pygal/graph/line.py index 437b96d..4d142d4 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -97,20 +97,20 @@ class Line(Graph): def _compute(self): x_pos = [x / (self._len - 1) for x in range(self._len) ] if self._len != 1 else [.5] # Center if only one value + for serie in self.series: - if not hasattr(serie, 'points'): - serie.points = [ - (x_pos[i], v) - for i, v in enumerate(serie.values)] - if self.interpolate: - serie.interpolated = self._interpolate(serie.values, x_pos) + serie.points = [ + (x_pos[i], v) + for i, v in enumerate(serie.values)] + if self.interpolate: + serie.interpolated = self._interpolate(serie.values, x_pos) if self.include_x_axis: - self._box.ymin = min(min(self._values), 0) - self._box.ymax = max(max(self._values), 0) + self._box.ymin = min(self._min, 0) + self._box.ymax = max(self._max, 0) else: - self._box.ymin = min(self._values) - self._box.ymax = max(self._values) + self._box.ymin = self._min + self._box.ymax = self._max y_pos = compute_scale( self._box.ymin, self._box.ymax, self.logarithmic diff --git a/pygal/graph/pyramid.py b/pygal/graph/pyramid.py new file mode 100644 index 0000000..58a5d48 --- /dev/null +++ b/pygal/graph/pyramid.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +# This file is part of pygal +# +# A python svg graph plotting library +# Copyright © 2012 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 . +""" +Pyramid chart + +""" + +from __future__ import division +from pygal.util import decorate, cut, compute_scale +from pygal.serie import PositiveValue +from pygal.graph.bar import Bar +from pygal.graph.horizontal import HorizontalGraph + + +class VerticalPyramid(Bar): + """Pyramid graph""" + + __value__ = PositiveValue + + def _format(self, value): + return super(VerticalPyramid, self)._format(abs(value)) + + def _compute(self): + positive_vals = zip(*[serie.values for serie in self.series + if serie.index % 2]) + negative_vals = zip(*[serie.values for serie in self.series + if not serie.index % 2]) + positive_sum = map(sum, positive_vals) or [0] + negative_sum = map(sum, negative_vals) + + self._box.ymax = max(max(positive_sum), max(negative_sum)) + self._box.ymin = - self._box.ymax + + x_pos = [x / self._len + for x in range(self._len + 1) + ] if self._len > 1 else [0, 1] # Center if only one value + y_pos = compute_scale( + self._box.ymin, self._box.ymax, self.logarithmic + ) if not self.y_labels else map(float, self.y_labels) + + self._x_ranges = zip(x_pos, x_pos[1:]) + + self._x_labels = self.x_labels and zip(self.x_labels, [ + sum(x_range) / 2 for x_range in self._x_ranges]) + self._y_labels = zip(map(self._format, y_pos), y_pos) + + def _plot(self): + stack_vals = [[0, 0] for i in range(self._len)] + for serie in self.series: + serie_node = self._serie(serie.index) + stack_vals = self.bar( + serie_node, serie, [ + tuple( + (self._x_ranges[i][j], + v * (-1 if serie.index % 2 else 1)) for j in range(2)) + for i, v in enumerate(serie.values)], + stack_vals) + + +class Pyramid(HorizontalGraph, VerticalPyramid): + """Horizontal Pyramid graph""" diff --git a/pygal/graph/radar.py b/pygal/graph/radar.py index 09901de..cb3b1e8 100644 --- a/pygal/graph/radar.py +++ b/pygal/graph/radar.py @@ -59,7 +59,7 @@ class Radar(Line): self.height - self.margin.y, self._box) - def _x_axis(self): + def _x_axis(self, draw_axes=True): if not self._x_labels: return @@ -86,7 +86,7 @@ class Radar(Line): text.attrib['transform'] = 'rotate(%f %s)' % ( deg(angle), format_(pos_text)) - def _y_axis(self): + def _y_axis(self, draw_axes=True): if not self._y_labels: return @@ -124,15 +124,14 @@ class Radar(Line): extended_vals, extended_x_pos, polar=True) self._box.margin *= 2 - self._box.xmin = self._box.ymin = 0 - self._box.xmax = self._box.ymax = self._rmax = max(self._values) + self._box.xmin = self._box.ymin = - self._max + self._box.xmax = self._box.ymax = self._rmax = self._max y_pos = compute_scale( - self._box.ymin, self._box.ymax, self.logarithmic, max_scale=8 + 0, self._box.ymax, self.logarithmic, max_scale=8 ) if not self.y_labels else map(int, self.y_labels) self._x_labels = self.x_labels and zip(self.x_labels, x_pos) self._y_labels = zip(map(self._format, y_pos), y_pos) - self._box.xmin = self._box.ymin = - self._box.ymax self.x_pos = x_pos self._self_close = True diff --git a/pygal/view.py b/pygal/view.py index f8c161e..8e249e2 100644 --- a/pygal/view.py +++ b/pygal/view.py @@ -47,9 +47,11 @@ class Box(object): """Chart boundings""" margin = .02 - def __init__(self): - self.xmin = self.ymin = 0 - self.xmax = self.ymax = 1 + def __init__(self, xmin=0, ymin=0, xmax=1, ymax=1): + self.xmin = xmin + self.ymin = ymin + self.xmax = xmax + self.ymax = ymax @property def width(self):