Browse Source

Many many things, noticeably interpolation

pull/8/head
Florian Mounier 13 years ago
parent
commit
82e506f8d8
  1. 157
      demo/moulinrouge/__init__.py
  2. 9
      demo/moulinrouge/templates/index.jinja2
  3. 2
      demo/moulinrouge/templates/svgs.jinja2
  4. 13
      demo/simple_test.py
  5. 11
      pygal/config.py
  6. 10
      pygal/graph/base.py
  7. 62
      pygal/graph/line.py
  8. 8
      pygal/graph/pie.py
  9. 49
      pygal/graph/radar.py
  10. 33
      pygal/graph/stackedline.py
  11. 35
      pygal/graph/xy.py
  12. 45
      pygal/interpolate.py
  13. 8
      pygal/view.py

157
demo/moulinrouge/__init__.py

@ -16,14 +16,19 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
from flask import Flask, render_template, url_for
from moulinrouge.data import labels, series
from flask import Flask, render_template
from logging import getLogger, INFO, DEBUG
import pygal
from pygal.config import Config
from pygal.style import styles
from pygal.util import cut
from pygal.style import styles, DefaultStyle
from pygal.serie import Serie
from base64 import (
urlsafe_b64encode as b64encode,
urlsafe_b64decode as b64decode)
import string
import random
import pickle
def random_label():
@ -54,26 +59,24 @@ def create_app():
getLogger('pygal').addHandler(handler)
getLogger('pygal').setLevel(DEBUG)
@app.route("/")
def index():
return render_template('index.jinja2')
def _random(data, order):
max = 10 ** order
min = 10 ** random.randrange(0, order)
@app.route("/all-<type>-<style>(fill=<fill>).svg")
def all_svg(type, style, fill):
data = random.randrange(1, 10)
order = random.randrange(1, 10)
series = []
for i in range(random.randrange(1, 10)):
values = [(
random_value((-max, min)[random.randrange(0, 2)], max),
random_value((-max, min)[random.randrange(0, 2)], max))
for i in range(data)]
series.append(Serie(random_label(), values, len(series)))
return series
def _random_series(type, data, order):
max = 10 ** order
min = 10 ** random.randrange(0, order)
config = Config()
config.width = 600
config.height = 400
config.fill = fill == 'True'
config.style = styles[style]
if type != 'Pie':
config.x_labels = [random_label() for i in range(data)]
config.title = "%d - %d" % (min, max)
g = getattr(pygal, type)(config)
series = []
for i in range(random.randrange(1, 10)):
if type == 'Pie':
values = random_value(min, max)
@ -85,59 +88,101 @@ def create_app():
else:
values = [random_value((-max, min)[random.randrange(1, 2)],
max) for i in range(data)]
g.add(random_label(), values)
return g.render_response()
series.append(Serie(random_label(), values, len(series)))
return series
@app.route("/")
def index():
return render_template('index.jinja2', styles=styles)
@app.route("/svg/<type>/<series>/<config>")
def svg(type, series, config):
graph = getattr(pygal, type)(pickle.loads(b64decode(str(config))))
graph.series = pickle.loads(b64decode(str(series)))
return graph.render_response()
@app.route("/all")
def all():
@app.route("/all/style=<style>")
def all(style=DefaultStyle):
width, height = 600, 400
svgs = [url_for('all_svg', type=type, style=style, fill=fill)
for style in styles
for fill in (False, True)
for type in ('Bar', 'Line', 'XY', 'StackedBar',
'StackedLine', 'HorizontalBar',
'HorizontalStackedBar',
'Pie', 'Radar')]
data = random.randrange(1, 10)
order = random.randrange(1, 10)
xy_series = _random(data, order)
other_series = []
for serie in xy_series:
other_series.append(
Serie(serie.title, cut(serie.values, 1), serie.index))
xy_series = b64encode(pickle.dumps(xy_series))
other_series = b64encode(pickle.dumps(other_series))
config = Config()
config.width = width
config.height = height
config.fill = True
config.style = styles[style]
labels = [random_label() for i in range(data)]
svgs = []
for type in ('Bar', 'Line', 'XY', 'StackedBar',
'StackedLine', 'HorizontalBar',
'HorizontalStackedBar',
'Pie', 'Radar'):
config.x_labels = labels if type != 'Pie' else None
svgs.append({'type': type,
'series': xy_series if type == 'XY' else other_series,
'config': b64encode(pickle.dumps(config))})
return render_template('svgs.jinja2',
svgs=svgs,
width=width,
height=height)
@app.route("/rotation[<int:angle>].svg")
def rotation_svg(angle):
config = Config()
config.width = 375
config.height = 245
config.x_labels = labels
config.x_label_rotation = angle
g = pygal.Line(config)
for serie, values in series.items():
g.add(serie, values)
g.add(serie, values)
return g.render_response()
@app.route("/rotation")
def rotation():
width, height = 375, 245
svgs = [url_for('rotation_svg', angle=angle)
for angle in range(0, 91, 5)]
config = Config()
config.width = width
config.height = height
config.fill = True
config.style = styles['neon']
data = random.randrange(1, 10)
order = random.randrange(1, 10)
series = b64encode(pickle.dumps(_random(type, data, order)))
labels = [random_label() for i in range(data)]
svgs = []
config.show_legend = bool(random.randrange(0, 1))
for angle in range(0, 91, 5):
config.title = "%d rotation" % angle
config.x_labels = labels
config.x_label_rotation = angle
svgs.append({'type': 'Bar',
'series': series,
'config': b64encode(pickle.dumps(config))})
return render_template('svgs.jinja2',
svgs=svgs,
width=width,
height=height)
@app.route("/bigline.svg")
def big_line_svg():
g = pygal.Line(600, 400)
g.x_labels = ['a', 'b', 'c', 'd']
g.add('serie', [11, 50, 133, 2])
return g.render_response()
@app.route("/bigline")
def big_line():
width, height = 900, 800
svgs = [url_for('big_line_svg')]
@app.route("/interpolation")
def interpolation():
width, height = 600, 400
config = Config()
config.width = width
config.height = height
config.fill = True
config.style = styles['neon']
data = random.randrange(1, 10)
order = random.randrange(1, 10)
series = b64encode(pickle.dumps(_random(type, data, order)))
svgs = []
for interpolation in (
'linear', 'slinear', 'nearest', 'zero', 'quadratic', 'cubic',
'krogh', 'barycentric', 'univariate', 4, 5, 6, 7, 8):
config.title = "%s interpolation" % interpolation
config.interpolate = interpolation
svgs.append({'type': 'StackedLine',
'series': series,
'config': b64encode(pickle.dumps(config))})
return render_template('svgs.jinja2',
svgs=svgs,
width=width,

9
demo/moulinrouge/templates/index.jinja2

@ -1,7 +1,12 @@
{% extends '_layout.jinja2' %}
{% block section %}
<a href="{{ url_for('all') }}">All types</a>
<dl>
<dt>All types</dt>
{% for style in styles %}
<dd><a href="{{ url_for('all', style=style) }}">{{ style }}</a></dd>
{% endfor %}
</dl>
<a href="{{ url_for('interpolation') }}">Interpolation</a>
<a href="{{ url_for('rotation') }}">Rotations test</a>
<a href="{{ url_for('big_line') }}">Big line</a>
{% endblock section %}

2
demo/moulinrouge/templates/svgs.jinja2

@ -3,7 +3,7 @@
{% block section %}
{% for svg in svgs %}
<figure>
<embed src="{{ svg }}" type="image/svg+xml" width="{{ width }}" height="{{ height }}" />
<embed src="{{ url_for('svg', type=svg.type, series=svg.series, config=svg.config) }}" type="image/svg+xml" width="{{ width }}" height="{{ height }}" />
<figcaption></figcaption>
</figure>
{% endfor %}

13
demo/simple_test.py

@ -72,8 +72,9 @@ hstackedbar.add('--->', rng3)
with open('out-horizontalstackedbar.svg', 'w') as f:
f.write(hstackedbar.render())
line = Line(Config(y_scale=.0005, fill=True, style=NeonStyle))
rng = range(-30, 31, 5)
line = Line(Config(y_scale=.0005, fill=True, style=NeonStyle,
interpolate='univariate'))
rng = range(-30, 31, 10)
line.add('test1', [cos(x / 10.) for x in rng])
line.add('test2', [sin(x / 10.) for x in rng])
line.add('test3', [cos(x / 10.) - sin(x / 10.) for x in rng])
@ -82,7 +83,8 @@ line.title = "Line test"
with open('out-line.svg', 'w') as f:
f.write(line.render())
stackedline = StackedLine(Config(y_scale=.0005, fill=True, style=NeonStyle))
stackedline = StackedLine(Config(y_scale=.0005, fill=True,
style=NeonStyle, interpolate='cubic'))
stackedline.add('test1', [1, 3, 2, 18, 2, 13, 8])
stackedline.add('test2', [4, 1, 0, 1, 3, 12, 3])
stackedline.add('test3', [9, 3, 2, 10, 8, 2, 3])
@ -91,9 +93,11 @@ stackedline.title = "Stackedline test"
with open('out-stackedline.svg', 'w') as f:
f.write(stackedline.render())
xy = XY(Config(x_scale=1))
xy = XY(Config(x_scale=1, fill=True, style=NeonStyle, interpolate='cubic'))
xy.add('test1', [(1981, 1), (2004, 2), (2003, 10), (2012, 8), (1999, -4)])
xy.add('test2', [(1988, -1), (1986, 12), (2007, 7), (2010, 4), (1999, 2)])
# xy.add('test2', [(1980, 0), (1985, 2), (1995, -2), (2005, 4), (2020, -4)])
# (2005, 6), (2010, -6), (2015, 3), (2020, -3), (2025, 0)])
xy.title = "XY test"
with open('out-xy.svg', 'w') as f:
f.write(xy.render())
@ -113,6 +117,7 @@ config.fill = True
config.style = NeonStyle
config.x_labels = (
'black', 'red', 'blue', 'yellow', 'orange', 'green', 'white')
config.interpolate = 'nearest'
radar = Radar(config)
radar.add('test', [1, 4, 1, 5, 7, 2, 5])
radar.add('test2', [10, 2, 7, 5, 1, 9, 4])

11
pygal/config.py

@ -59,8 +59,17 @@ class Config(object):
rounded_bars = False
# Always include x axis
include_x_axis = False
# Fill areas
# Fill areas under lines
fill = False
# Line dots (set it to false to get a scatter plot)
stroke = True
# Interpolation, this requires scipy module
# May be any of ‘linear’, ’nearest’, ‘zero’, ‘slinear’, ‘quadratic, ‘cubic’
# 'krogh', 'barycentric', 'univariate', or an integer specifying the order
# of the spline interpolator
interpolate = None
# Number of interpolated points between two values
interpolation_precision = 250
def __init__(self, **kwargs):
"""Can be instanciated with config kwargs"""

10
pygal/graph/base.py

@ -102,9 +102,13 @@ class BaseGraph(object):
self.series.append(Serie(title, values, len(self.series)))
def render(self):
if len(self.series) == 0 or sum(
map(len, map(lambda s: s.values, self.series))) == 0:
return "No data"
if len(self.series) == 0:
return
for serie in self.series:
if not hasattr(serie.values, '__iter__'):
serie.values = [serie.values]
if sum(map(len, map(lambda s: s.values, self.series))) == 0:
return
try:
self.validate()
self._draw()

62
pygal/graph/line.py

@ -17,36 +17,63 @@
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
from pygal.graph.graph import Graph
from pygal.util import cached_property
from pygal.interpolate import interpolation
class Line(Graph):
"""Line graph"""
def __init__(self, *args, **kwargs):
super(Line, self).__init__(*args, **kwargs)
self._line_close = False
def _get_value(self, values, i):
return str(values[i][1])
def line(self, serie_node, values):
view_values = map(self.view, values)
@cached_property
def _values(self):
if self.interpolate:
return [val[1] for serie in self.series
for val in serie.interpolated]
else:
return [val[1] for serie in self.series
for val in serie.points]
def _fill(self, values):
zero = self.view.y(min(max(0, self._box.ymin), self._box.ymax))
return ([(values[0][0], zero)] +
values +
[(values[-1][0], zero)])
def line(self, serie_node, serie):
view_values = map(self.view, serie.points)
dots = self.svg.node(serie_node, class_="dots")
for i, (x, y) in enumerate(view_values):
dot = self.svg.node(dots, class_='dot')
self.svg.node(dot, 'circle', cx=x, cy=y, r=2.5)
self.svg.node(dot, 'text', x=x, y=y
).text = self._get_value(values, i)
if self.fill:
zero = self.view.y(min(max(0, self._box.ymin), self._box.ymax))
view_values = ([(view_values[0][0], zero)] +
view_values +
[(view_values[-1][0], zero)])
self.svg.line(
serie_node, view_values, class_='line', close=self._line_close)
).text = self._get_value(serie.points, i)
if self.stroke:
if self.interpolate:
view_values = map(self.view, serie.interpolated)
if self.fill:
view_values = self._fill(view_values)
self.svg.line(
serie_node, view_values, class_='line')
def _compute(self):
self._x_pos = [x / float(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 = [
(self._x_pos[i], v)
for i, v in enumerate(serie.values)]
if self.interpolate:
interpolate = interpolation(
self._x_pos, serie.values, kind=self.interpolate)
p = float(self.interpolation_precision)
serie.interpolated = [(x / p, float(interpolate(x / p)))
for x in range(int(p + 1))]
if self.include_x_axis:
self._box.ymin = min(min(self._values), 0)
self._box.ymax = max(max(self._values), 0)
@ -54,8 +81,6 @@ class Line(Graph):
self._box.ymin = min(self._values)
self._box.ymax = max(self._values)
self._x_pos = [x / float(self._len - 1) for x in range(self._len)
] if self._len != 1 else [.5] # Center if only one value
self._y_pos = self._pos(self._box.ymin, self._box.ymax, self.y_scale
) if not self.y_labels else map(int, self.y_labels)
@ -64,7 +89,4 @@ class Line(Graph):
def _plot(self):
for serie in self.series:
self.line(
self._serie(serie.index), [
(self._x_pos[i], v)
for i, v in enumerate(serie.values)])
self.line(self._serie(serie.index), serie)

8
pygal/graph/pie.py

@ -49,11 +49,15 @@ class Pie(Graph):
y=center[1] - text_r * sin(text_angle),
).text = '{:.2%}'.format(perc)
def add(self, title, value):
self.series.append(Serie(title, [value], len(self.series)))
def _compute(self):
for serie in self.series:
serie.values = [max(serie.values[0], 0)]
return super(Pie, self)._compute()
def _plot(self):
total = float(sum(serie.values[0] for serie in self.series))
if total == 0:
return
current_angle = 0
for serie in self.series:
val = serie.values[0]

49
pygal/graph/radar.py

@ -18,16 +18,28 @@
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
from pygal.graph.line import Line
from pygal.view import PolarView
from pygal.util import deg
from pygal.util import deg, cached_property
from pygal.interpolate import interpolation
from math import cos, pi
class Radar(Line):
"""Kiviat graph"""
def _fill(self, values):
return values
def _get_value(self, values, i):
return str(values[i][0])
@cached_property
def _values(self):
if self.interpolate:
return [val[0] for serie in self.series
for val in serie.interpolated]
else:
return super(Line, self)._values
def _set_view(self):
self.view = PolarView(
self.width - self.margin.x,
@ -80,24 +92,37 @@ class Radar(Line):
).text = label
def _compute(self):
delta = 2 * pi / float(self._len)
self._x_pos = [.5 * pi + i * delta for i in range(self._len + 1)]
for serie in self.series:
vals = list(serie.values)
vals.append(vals[0])
serie.points = [
(v, self._x_pos[i])
for i, v in enumerate(vals)]
if self.interpolate:
extend = 2
extended_x_pos = (
[.5 * pi + i * delta for i in range(-extend, 0)] +
self._x_pos +
[.5 * pi + i * delta for i in range(
self._len + 1, self._len + 1 + extend)])
extended_vals = vals[-extend:] + vals + vals[:extend]
interpolate = interpolation(
extended_x_pos, extended_vals, kind=self.interpolate)
serie.interpolated = []
p = float(self.interpolation_precision)
for s in range(int(p + 1)):
x = .5 * pi + 2 * pi * (s / p)
serie.interpolated.append((float(interpolate(x)), x))
self._box._margin *= 2
self._box.xmin = self._box.ymin = 0
self._box.xmax = self._box.ymax = self._rmax = max(self._values)
x_step = len(self.series[0].values)
delta = 2 * pi / float(len(self.x_labels))
self._x_pos = [.5 * pi - i * delta for i in range(x_step)]
self._y_pos = self._pos(
self._box.ymin, self._box.ymax, self.y_scale, 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, self._x_pos)
self._y_labels = zip(map(str, self._y_pos), self._y_pos)
self._box.xmin = self._box.ymin = - self._box.ymax
self._line_close = True
def _plot(self):
for serie in self.series:
serie_node = self._serie(serie.index)
self.line(serie_node, [
(v, self._x_pos[i])
for i, v in enumerate(serie.values)])

33
pygal/graph/stackedline.py

@ -17,22 +17,33 @@
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
from pygal.graph.line import Line
from pygal.interpolate import interpolation
class StackedLine(Line):
"""Stacked Line graph"""
@property
def _values(self):
sums = map(sum, zip(*[serie.values for serie in self.series]))
return sums + super(StackedLine, self)._values
def _fill(self, values):
if not hasattr(self, '_previous_line'):
self._previous_line = values
return super(StackedLine, self)._fill(values)
new_values = values + list(reversed(self._previous_line))
self._previous_line = values
return new_values
def _plot(self):
accumulation = map(sum, zip(*[serie.values for serie in self.series]))
def _compute(self):
self._x_pos = [x / float(self._len - 1) for x in range(self._len)
] if self._len != 1 else [.5] # Center if only one value
accumulation = [0] * self._len
for serie in self.series:
self.line(
self._serie(serie.index), [
accumulation = map(sum, zip(accumulation, serie.values))
serie.points = [
(self._x_pos[i], v)
for i, v in enumerate(accumulation)])
accumulation = map(sum, zip(accumulation,
[-v for v in serie.values]))
for i, v in enumerate(accumulation)]
if self.interpolate:
interpolate = interpolation(
self._x_pos, accumulation, kind=self.interpolate)
p = float(self.interpolation_precision)
serie.interpolated = [(x / p, float(interpolate(x / p)))
for x in range(int(p + 1))]
return super(StackedLine, self)._compute()

35
pygal/graph/xy.py

@ -17,6 +17,7 @@
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
from pygal.graph.line import Line
from pygal.interpolate import interpolation
class XY(Line):
@ -26,20 +27,38 @@ class XY(Line):
return str(values[i])
def _compute(self):
for serie in self.series:
serie.values = sorted(serie.values, key=lambda x: x[0])
xvals = [val[0] for serie in self.series for val in serie.values]
yvals = [val[1] for serie in self.series for val in serie.values]
xmin = min(xvals)
for serie in self.series:
serie.points = sorted(serie.values, key=lambda x: x[0])
if self.interpolate:
vals = zip(*serie.points)
interpolate = interpolation(
vals[0], vals[1], kind=self.interpolate)
serie_xmin = min(vals[0])
serie_xmax = max(vals[0])
serie.interpolated = []
r = (max(xvals) - xmin)
p = float(self.interpolation_precision)
for s in range(int(p + 1)):
x = xmin + r * (s / p)
if serie_xmin <= x <= serie_xmax:
serie.interpolated.append((x, float(interpolate(x))))
if self.interpolate:
xvals = [val[0]
for serie in self.series
for val in serie.interpolated]
yvals = [val[1]
for serie in self.series
for val in serie.interpolated]
self._box.xmin, self._box.xmax = min(xvals), max(xvals)
self._box.ymin, self._box.ymax = min(yvals), max(yvals)
x_pos = self._pos(self._box.xmin, self._box.xmax, self.x_scale)
y_pos = self._pos(self._box.ymin, self._box.ymax, self.y_scale)
self._x_labels = zip(map(str, x_pos), x_pos)
self._y_labels = zip(map(str, y_pos), y_pos)
def _plot(self):
for serie in self.series:
self.line(
self._serie(serie.index), serie.values)

45
pygal/interpolate.py

@ -0,0 +1,45 @@
# -*- 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/>.
from pygal.util import ident
try:
import scipy
from scipy import interpolate
except:
scipy = None
def interpolation(x, y, kind):
assert scipy != None, 'You must have scipy installed to use interpolation'
order = None
if len(x) < 2:
return ident
if isinstance(kind, int):
order = kind
elif kind in ['zero', 'slinear', 'quadratic', 'cubic']:
order = {'nearest': 0, 'zero': 0, 'slinear': 1,
'quadratic': 2, 'cubic': 3}[kind]
if order and len(x) <= order:
kind = len(x) - 1
if kind == 'krogh':
return interpolate.KroghInterpolator(x, y)
elif kind == 'barycentric':
return interpolate.BarycentricInterpolator(x, y)
elif kind == 'univariate':
return interpolate.InterpolatedUnivariateSpline(x, y)
return interpolate.interp1d(x, y, kind=kind)

8
pygal/view.py

@ -88,8 +88,8 @@ class View(object):
class PolarView(View):
def __call__(self, rtheta):
r, theta = rtheta
r = max(r, 0)
def __call__(self, rhotheta):
rho, theta = rhotheta
rho = max(rho, 0)
return super(PolarView, self).__call__(
(r * cos(theta), r * sin(theta)))
(rho * cos(theta), rho * sin(theta)))

Loading…
Cancel
Save