Browse Source

Add funnel and pyramid

pull/8/head
Florian Mounier 13 years ago
parent
commit
8453a94f7b
  1. 26
      demo/simple_test.py
  2. 6
      pygal/__init__.py
  3. 9
      pygal/graph/bar.py
  4. 5
      pygal/graph/base.py
  5. 97
      pygal/graph/funnel.py
  6. 10
      pygal/graph/line.py
  7. 77
      pygal/graph/pyramid.py
  8. 11
      pygal/graph/radar.py
  9. 8
      pygal/view.py

26
demo/simple_test.py

@ -17,13 +17,35 @@
# #
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
import sys import time
from pygal import * from pygal import *
from pygal.style import * from pygal.style import *
from math import cos, sin from math import cos, sin
lnk = lambda v, l=None: {'value': v, 'xlink': 'javascript:alert("Test %s")' % v, 'label': l} 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 = Dot()
dot.x_labels = map(str, range(4)) 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.title = "Radar test"
radar.render_to_file('out-radar.svg') radar.render_to_file('out-radar.svg')
print "Ok (%dms)" % (1000 * (time.time() - t_start))

6
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.config import Config
from pygal.graph.bar import Bar 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.horizontal import HorizontalStackedBar
from pygal.graph.line import Line from pygal.graph.line import Line
from pygal.graph.pie import Pie 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.radar import Radar
from pygal.graph.stackedbar import StackedBar from pygal.graph.stackedbar import StackedBar
from pygal.graph.stackedline import StackedLine from pygal.graph.stackedline import StackedLine
@ -39,11 +41,13 @@ from pygal.graph.xy import XY
#: List of all chart types #: List of all chart types
CHARTS = [ CHARTS = [
Bar, Bar,
Funnel,
Dot, Dot,
HorizontalBar, HorizontalBar,
HorizontalStackedBar, HorizontalStackedBar,
Line, Line,
Pie, Pie,
Pyramid,
Radar, Radar,
StackedBar, StackedBar,
StackedLine, StackedLine,

9
pygal/graph/bar.py

@ -115,11 +115,10 @@ class Bar(Graph):
return stack_vals return stack_vals
def _compute(self): def _compute(self):
self._box.ymin = min(min(self._values), self.zero) self._box.ymin = min(self._min, self.zero)
self._box.ymax = max(max(self._values), self.zero) self._box.ymax = max(self._max, self.zero)
x_step = len(self.series[0].values) x_pos = [x / self._len for x in range(self._len + 1)
x_pos = [x / x_step for x in range(x_step + 1) ] if self._len > 1 else [0, 1] # Center if only one value
] if x_step > 1 else [0, 1] # Center if only one value
y_pos = compute_scale( y_pos = compute_scale(
self._box.ymin, self._box.ymax, self.logarithmic self._box.ymin, self._box.ymax, self.logarithmic
) if not self.y_labels else map(float, self.y_labels) ) if not self.y_labels else map(float, self.y_labels)

5
pygal/graph/base.py

@ -125,6 +125,11 @@ class BaseGraph(object):
"""Getter for the maximum series size""" """Getter for the maximum series size"""
return max([len(serie.values) for serie in self.series]) 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 @cached_property
def _max(self): def _max(self):
"""Getter for the maximum series value""" """Getter for the maximum series value"""

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

10
pygal/graph/line.py

@ -97,8 +97,8 @@ class Line(Graph):
def _compute(self): def _compute(self):
x_pos = [x / (self._len - 1) for x in range(self._len) x_pos = [x / (self._len - 1) for x in range(self._len)
] if self._len != 1 else [.5] # Center if only one value ] if self._len != 1 else [.5] # Center if only one value
for serie in self.series: for serie in self.series:
if not hasattr(serie, 'points'):
serie.points = [ serie.points = [
(x_pos[i], v) (x_pos[i], v)
for i, v in enumerate(serie.values)] for i, v in enumerate(serie.values)]
@ -106,11 +106,11 @@ class Line(Graph):
serie.interpolated = self._interpolate(serie.values, x_pos) serie.interpolated = self._interpolate(serie.values, x_pos)
if self.include_x_axis: if self.include_x_axis:
self._box.ymin = min(min(self._values), 0) self._box.ymin = min(self._min, 0)
self._box.ymax = max(max(self._values), 0) self._box.ymax = max(self._max, 0)
else: else:
self._box.ymin = min(self._values) self._box.ymin = self._min
self._box.ymax = max(self._values) self._box.ymax = self._max
y_pos = compute_scale( y_pos = compute_scale(
self._box.ymin, self._box.ymax, self.logarithmic self._box.ymin, self._box.ymax, self.logarithmic

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

11
pygal/graph/radar.py

@ -59,7 +59,7 @@ class Radar(Line):
self.height - self.margin.y, self.height - self.margin.y,
self._box) self._box)
def _x_axis(self): def _x_axis(self, draw_axes=True):
if not self._x_labels: if not self._x_labels:
return return
@ -86,7 +86,7 @@ class Radar(Line):
text.attrib['transform'] = 'rotate(%f %s)' % ( text.attrib['transform'] = 'rotate(%f %s)' % (
deg(angle), format_(pos_text)) deg(angle), format_(pos_text))
def _y_axis(self): def _y_axis(self, draw_axes=True):
if not self._y_labels: if not self._y_labels:
return return
@ -124,15 +124,14 @@ class Radar(Line):
extended_vals, extended_x_pos, polar=True) extended_vals, extended_x_pos, polar=True)
self._box.margin *= 2 self._box.margin *= 2
self._box.xmin = self._box.ymin = 0 self._box.xmin = self._box.ymin = - self._max
self._box.xmax = self._box.ymax = self._rmax = max(self._values) self._box.xmax = self._box.ymax = self._rmax = self._max
y_pos = compute_scale( 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) ) 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._x_labels = self.x_labels and zip(self.x_labels, x_pos)
self._y_labels = zip(map(self._format, y_pos), y_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.x_pos = x_pos
self._self_close = True self._self_close = True

8
pygal/view.py

@ -47,9 +47,11 @@ class Box(object):
"""Chart boundings""" """Chart boundings"""
margin = .02 margin = .02
def __init__(self): def __init__(self, xmin=0, ymin=0, xmax=1, ymax=1):
self.xmin = self.ymin = 0 self.xmin = xmin
self.xmax = self.ymax = 1 self.ymin = ymin
self.xmax = xmax
self.ymax = ymax
@property @property
def width(self): def width(self):

Loading…
Cancel
Save