From b0040997704173756e2b1d15c8e152d293c65d5e Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Tue, 20 Mar 2012 15:08:29 +0100 Subject: [PATCH] Support different series array sizes --- demo/simple_test.py | 40 +++++++++++++++++++++++++------------- pygal/__init__.py | 2 +- pygal/graph/bar.py | 4 +++- pygal/graph/base.py | 21 ++++++++------------ pygal/graph/line.py | 21 ++++++++++++++------ pygal/graph/radar.py | 1 + pygal/graph/stackedbar.py | 6 ++++-- pygal/graph/stackedline.py | 13 ++++++++++--- pygal/graph/xy.py | 12 +++++++++--- pygal/interpolate.py | 11 ++++++++--- pygal/style.py | 4 ++-- pygal/svg.py | 9 +++++++-- pygal/view.py | 8 ++++++++ setup.py | 3 +-- 14 files changed, 103 insertions(+), 52 deletions(-) diff --git a/demo/simple_test.py b/demo/simple_test.py index b989e49..6445fce 100755 --- a/demo/simple_test.py +++ b/demo/simple_test.py @@ -27,16 +27,19 @@ bar = Bar() rng = [-6, -19, 0, -1, 2] bar.add('test1', rng) bar.add('test2', map(abs, rng)) +bar.add('inc', [None, 1, None, 2]) bar.x_labels = map(str, rng) bar.title = "Bar test" bar.fill = True bar.render_to_file('out-bar.svg') hbar = HorizontalBar() -rng = [18, 9, 7, 3, 1, 0, -5] +rng = [18, 9, 7, 3, 1, None, -5] hbar.add('test1', rng) rng2 = [16, 14, 10, 9, 7, 3, -1] hbar.add('test2', rng2) +rng3 = [123, None, None, 4, None, 6] +hbar.add('test3', rng3) hbar.x_labels = map( lambda x: '%s / %s' % x, zip(map(str, rng), map(str, rng2))) hbar.title = "Horizontal Bar test" @@ -58,6 +61,7 @@ stackedbar = StackedBar(config) stackedbar.add('@@@@@@@', rng) stackedbar.add('++++++', rng2) stackedbar.add('--->', rng3) +stackedbar.add('None', [None, 42, 42]) stackedbar.render_to_file('out-stackedbar.svg') config.title = "Horizontal Stacked Bar test" @@ -68,32 +72,39 @@ hstackedbar.add('--->', rng3) hstackedbar.render_to_file('out-horizontalstackedbar.svg') line = Line(Config(y_scale=.0005, style=NeonStyle, - zero=1, interpolate='cubic', + zero=1, human_readable=True, logarithmic=True)) # rng = range(-30, 31, 1) # line.add('test1', [1000 ** cos(x / 10.) for x in rng]) # line.add('test2', [1000 ** sin(x / 10.) for x in rng]) # line.add('test3', [1000 ** (cos(x / 10.) - sin(x / 10.)) for x in rng]) -rng = range(1, 2000, 50) -line.add('x', rng) -line.add('10^x', map(lambda x: 10 ** (x / 333.), rng)) -line.add('10^10^x', map(lambda x: ((x / 333.) ** (x / 333.)), rng)) +rng = range(1, 2000, 25) +# line.add('x', rng) +# line.add('x', rng) +# line.add('10^10^x', map(lambda x: ((x / 333.) ** (x / 333.)), rng)) +# line.add('None', [None, None, None, 12, 31, 11, None, None, 12, 14]) +line.add('2', [None, None, 2, 4, 8, None, 14, 10, None]) +line.add('1', [1, 5, 3, 4, 6, 12, 13, 7, 2]) line.x_labels = map(str, rng) line.title = "Line test" +line.interpolate = "cubic" +line.interpolation_precision = 200 line.render_to_file('out-line.svg') stackedline = StackedLine(Config(y_scale=.0005, fill=True)) -stackedline.add('test1', [1, 3, 2, 18, 2, 13, 8]) -stackedline.add('test2', [4, 1, 10, 1, 3, 12, 3]) -stackedline.add('test3', [9, 3, 2, 10, 8, 2, 3]) +stackedline.add('test1', [1, 3, 2, None, 2, 13, 2, 5, 8]) +stackedline.add('test2', [4, 1, 1, 3, 12, 3]) +stackedline.add('test3', [9, 3, 2, 10, 8, 2]) stackedline.x_labels = ['a', 'b', 'c', 'd', 'e', 'f', 'g'] stackedline.title = "Stackedline test" +stackedline.interpolate = "cubic" stackedline.render_to_file('out-stackedline.svg') 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('test1', [(1981, 1), (1999, -4), (2001, 2), (2003, 10), (2012, 8)]) +xy.add('test2', [(1988, -1), (1986, 12), (2007, 7), (2010, 4)]) +xy.add('test2', [(None, None), (None, 12), (2007, None), (2002.3, 12)]) # 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" @@ -101,10 +112,11 @@ xy.render_to_file('out-xy.svg') pie = Pie(Config(style=NeonStyle)) pie.add('test', [11, 8, 21]) -pie.add('test2', [29, 21, 9]) +pie.add('test2', [29, None, 9]) pie.add('test3', [24, 10, 32]) pie.add('test4', [20, 18, 9]) pie.add('test5', [17, 5, 10]) +pie.add('test6', [None, None, 10]) pie.title = "Pie test" pie.render_to_file('out-pie.svg') @@ -115,8 +127,8 @@ 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]) +radar.add('test', [1, 4, 1, 5, None, 2, 5]) +radar.add('test2', [10, 2, 0, 5, 1, 9, 4]) radar.title = "Radar test" radar.render_to_file('out-radar.svg') diff --git a/pygal/__init__.py b/pygal/__init__.py index 0a89ccd..1d98d04 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -16,7 +16,7 @@ # # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . -__version__ = '0.9.13-dev' +__version__ = '0.9.15' from collections import namedtuple from pygal.graph.bar import Bar diff --git a/pygal/graph/bar.py b/pygal/graph/bar.py index 63020ac..b9e2820 100644 --- a/pygal/graph/bar.py +++ b/pygal/graph/bar.py @@ -31,11 +31,13 @@ class Bar(Graph): """Project range""" t, T = rng fun = swap if self._horizontal else ident - return (self.view(fun(t)), self.view(fun(T))) + return self.view(fun(t)), self.view(fun(T)) bars = self.svg.node(serie_node['plot'], class_="bars") view_values = map(view, values) for i, ((x, y), (X, Y)) in enumerate(view_values): + if None in (x, y): + continue # x and y are left range coords and X, Y right ones val = self.format(values[i][1][1]) if self._horizontal: diff --git a/pygal/graph/base.py b/pygal/graph/base.py index 7753f3e..4c82e2e 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -49,12 +49,13 @@ class BaseGraph(object): step = float(10 ** order) while (max_ - min_) / step > max_scale: step *= 2. - positions = set() + positions = [] position = round_to_scale(min_, step) while position < (max_ + step): rounded = round_to_scale(position, scale) if min_ <= rounded <= max_: - positions.add(rounded) + if rounded not in positions: + positions.append(rounded) position += step if len(positions) < 2: return [min_, max_] @@ -104,11 +105,14 @@ class BaseGraph(object): @cached_property def _values(self): - return [val for serie in self.series for val in serie.values] + return [val + for serie in self.series + for val in serie.values + if val != None] @cached_property def _len(self): - return len(self.series[0].values) + return max([len(serie.values) for serie in self.series]) def _draw(self): self._compute() @@ -127,18 +131,9 @@ class BaseGraph(object): serie.values = [serie.values] if sum(map(len, map(lambda s: s.values, self.series))) == 0: return self.svg.render(no_data=True) - self.validate() self._draw() return self.svg.render() - def validate(self): - if self.x_labels: - assert len(self.series[0].values) == len(self.x_labels), ( - 'Labels and series must have the same length') - for serie in self.series: - assert len(self.series[0].values) == len(serie.values), ( - 'All series must have the same length') - def _in_browser(self): from lxml.html import open_in_browser self._draw() diff --git a/pygal/graph/line.py b/pygal/graph/line.py index 6dd495f..fecd884 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -19,6 +19,7 @@ from pygal.graph.graph import Graph from pygal.util import cached_property from pygal.interpolate import interpolation +from math import isnan class Line(Graph): @@ -30,11 +31,15 @@ class Line(Graph): @cached_property def _values(self): if self.interpolate: - return [val[1] for serie in self.series - for val in serie.interpolated] + return [val[1] + for serie in self.series + for val in serie.interpolated + if val[1] != None] else: - return [val[1] for serie in self.series - for val in serie.points] + return [val[1] + for serie in self.series + for val in serie.points + if val[1] != None] def _fill(self, values): zero = self.view.y(min(max(self.zero, self._box.ymin), self._box.ymax)) @@ -46,6 +51,8 @@ class Line(Graph): view_values = map(self.view, serie.points) if self.show_dots: for i, (x, y) in enumerate(view_values): + if None in (x, y): + continue dots = self.svg.node(serie_node['overlay'], class_="dots") val = self._get_value(serie.points, i) self.svg.node(dots, 'circle', cx=x, cy=y, r=2.5, @@ -81,8 +88,10 @@ class Line(Graph): 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))] + serie.interpolated = [ + (x / p, float(interpolate(x / p))) + for x in range(int(p + 1)) + if not isnan(float(interpolate(x / p)))] if self.include_x_axis: self._box.ymin = min(min(self._values), 0) self._box.ymax = max(max(self._values), 0) diff --git a/pygal/graph/radar.py b/pygal/graph/radar.py index 4948e23..db6f2b8 100644 --- a/pygal/graph/radar.py +++ b/pygal/graph/radar.py @@ -97,6 +97,7 @@ class Radar(Line): for serie in self.series: vals = list(serie.values) vals.append(vals[0]) + vals = [val if val != None else 0 for val in vals] serie.points = [ (v, self._x_pos[i]) for i, v in enumerate(vals)] diff --git a/pygal/graph/stackedbar.py b/pygal/graph/stackedbar.py index 60e980e..f6a550d 100644 --- a/pygal/graph/stackedbar.py +++ b/pygal/graph/stackedbar.py @@ -24,9 +24,11 @@ class StackedBar(Bar): def _compute(self): transposed = zip(*[serie.values for serie in self.series]) - positive_vals = [sum([val if val > 0 else 0 for val in vals]) + positive_vals = [sum([val + if val != None and val > 0 else 0 for val in vals]) for vals in transposed] - negative_vals = [sum([val if val < 0 else 0 for val in vals]) + negative_vals = [sum([val + if val != None and val < 0 else 0 for val in vals]) for vals in transposed] self._box.ymin, self._box.ymax = ( diff --git a/pygal/graph/stackedline.py b/pygal/graph/stackedline.py index 7642de3..513d5cd 100644 --- a/pygal/graph/stackedline.py +++ b/pygal/graph/stackedline.py @@ -18,6 +18,8 @@ # along with pygal. If not, see . from pygal.graph.line import Line from pygal.interpolate import interpolation +from math import isnan +from itertools import izip_longest class StackedLine(Line): @@ -36,7 +38,10 @@ class StackedLine(Line): ] if self._len != 1 else [.5] # Center if only one value accumulation = [0] * self._len for serie in self.series: - accumulation = map(sum, zip(accumulation, serie.values)) + accumulation = map(sum, izip_longest( + accumulation, [val + if val != None else 0 + for val in serie.values], fillvalue=0)) serie.points = [ (self._x_pos[i], v) for i, v in enumerate(accumulation)] @@ -44,6 +49,8 @@ class StackedLine(Line): 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))] + serie.interpolated = [ + (x / p, float(interpolate(x / p))) + for x in range(int(p + 1)) + if not isnan(float(interpolate(x / p)))] return super(StackedLine, self)._compute() diff --git a/pygal/graph/xy.py b/pygal/graph/xy.py index 76ab3dd..702ed74 100644 --- a/pygal/graph/xy.py +++ b/pygal/graph/xy.py @@ -27,12 +27,18 @@ class XY(Line): return str(values[i]) def _compute(self): - 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] + xvals = [val[0] + for serie in self.series + for val in serie.values + if val[0] != None] + yvals = [val[1] + for serie in self.series + for val in serie.values + if val[1] != None] xmin = min(xvals) for serie in self.series: - serie.points = sorted(serie.values, key=lambda x: x[0]) + serie.points = serie.values if self.interpolate: vals = zip(*serie.points) interpolate = interpolation( diff --git a/pygal/interpolate.py b/pygal/interpolate.py index 942ff17..86c1a61 100644 --- a/pygal/interpolate.py +++ b/pygal/interpolate.py @@ -27,13 +27,18 @@ except: def interpolation(x, y, kind): assert scipy != None, 'You must have scipy installed to use interpolation' order = None + if len(y) < len(x): + x = x[:len(y)] + + x, y = zip(*filter(lambda t: None not in t, zip(x, y))) + if len(x) < 2: return ident if isinstance(kind, int): order = kind - elif kind in ['zero', 'slinear', 'quadratic', 'cubic']: + elif kind in ['zero', 'slinear', 'quadratic', 'cubic', 'univariate']: order = {'nearest': 0, 'zero': 0, 'slinear': 1, - 'quadratic': 2, 'cubic': 3}[kind] + 'quadratic': 2, 'cubic': 3, 'univariate': 3}[kind] if order and len(x) <= order: kind = len(x) - 1 if kind == 'krogh': @@ -42,4 +47,4 @@ def interpolation(x, y, kind): return interpolate.BarycentricInterpolator(x, y) elif kind == 'univariate': return interpolate.InterpolatedUnivariateSpline(x, y) - return interpolate.interp1d(x, y, kind=kind) + return interpolate.interp1d(x, y, kind=kind, bounds_error=False) diff --git a/pygal/style.py b/pygal/style.py index 812e08d..0edae19 100644 --- a/pygal/style.py +++ b/pygal/style.py @@ -85,7 +85,7 @@ DarkSolarizedStyle = Style( background='#073642', plot_background='#002b36', foreground='#839496', - foreground_light='#93a1a1', + foreground_light='#fdf6e3', foreground_dark='#657b83', opacity='.66', opacity_hover='.9', @@ -96,7 +96,7 @@ LightSolarizedStyle = Style( background='#fdf6e3', plot_background='#eee8d5', foreground='#657b83', - foreground_light='#586e75', + foreground_light='#073642', foreground_dark='#073642', opacity='.6', opacity_hover='.9', diff --git a/pygal/svg.py b/pygal/svg.py index a88e984..607ffff 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -86,8 +86,13 @@ class Svg(object): def line(self, node, coords, close=False, **kwargs): root = 'M%s L%s Z' if close else 'M%s L%s' - origin = self.format(coords[0]) - line = ' '.join(map(self.format, coords[1:])) + origin_index = 0 + while None in coords[origin_index]: + origin_index += 1 + origin = self.format(coords[origin_index]) + line = ' '.join([self.format(c) + for c in coords[origin_index + 1:] + if None not in c]) self.node(node, 'path', d=root % (origin, line), **kwargs) diff --git a/pygal/view.py b/pygal/view.py index 223d0d5..785650b 100644 --- a/pygal/view.py +++ b/pygal/view.py @@ -76,9 +76,13 @@ class View(object): self.box.fix() def x(self, x): + if x == None: + return None return self.width * (x - self.box.xmin) / float(self.box.width) def y(self, y): + if y == None: + return None return (self.height - self.height * (y - self.box.ymin) / float(self.box.height)) @@ -89,6 +93,8 @@ class View(object): class PolarView(View): def __call__(self, rhotheta): + if None in rhotheta: + return None, None rho, theta = rhotheta rho = max(rho, 0) return super(PolarView, self).__call__( @@ -107,6 +113,8 @@ class LogView(View): self.box.fix() def y(self, y): + if y == None: + return None return (self.height - self.height * (log10(y) - self.log10_ymin) / float(self.log10_ymax - self.log10_ymin)) diff --git a/setup.py b/setup.py index 25c1049..3ae7ff1 100644 --- a/setup.py +++ b/setup.py @@ -18,11 +18,10 @@ # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . from setuptools import setup, find_packages -import pygal setup( name="pygal", - version=pygal.__version__.replace('-dev', ''), + version='0.9.15', description="A python svg graph plotting library", author="Kozea", author_email="florian.mounier@kozea.fr",