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):