From 022efcb7f863c932400f020432fb0e30bbe04f56 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Thu, 9 Feb 2012 16:30:01 +0100 Subject: [PATCH] Add stack bar and pie bugfix --- demo/moulinrouge/__init__.py | 45 ++++++++++------------------ out.py | 54 +++++++++++++++++++++++++++++++++ pygal/__init__.py | 1 + pygal/base.py | 15 +++++++++- pygal/stackedbar.py | 38 +++++++++++++++++++++++ pygal/svg.py | 58 ++++++++++++++++++++++++++++++------ relauncher | 7 +++-- 7 files changed, 176 insertions(+), 42 deletions(-) create mode 100755 out.py create mode 100644 pygal/stackedbar.py diff --git a/demo/moulinrouge/__init__.py b/demo/moulinrouge/__init__.py index d2c6ae5..8e09cd5 100644 --- a/demo/moulinrouge/__init__.py +++ b/demo/moulinrouge/__init__.py @@ -2,13 +2,9 @@ from flask import Flask, Response, render_template, url_for from log_colorizer import make_colored_stream_handler from logging import getLogger, INFO, DEBUG -from moulinrouge.data import labels, series -# from pygal.bar import VerticalBar, HorizontalBar -from pygal.line import Line -from pygal.bar import Bar +import pygal from pygal.config import Config from pygal.style import styles -# from pygal.pie import Pie import string import random @@ -25,14 +21,6 @@ def random_value(min=0, max=15): return random.randrange(min, max, 1) -# def generate_vbar(**opts): -# g = VerticalBar(labels, opts) -# for serie, values in series.items(): -# g.add_data({'data': values, 'title': serie}) - -# return Response(g.burn(), mimetype='image/svg+xml') - - def create_app(): """Creates the pygal test web app""" @@ -57,23 +45,22 @@ def create_app(): config.width = 600 config.height = 400 config.style = styles[style] - config.x_labels = [random_label() for i in range(data)] + if type != 'Pie': + config.x_labels = [random_label() for i in range(data)] config.title = "%d - %d" % (min, max) - if type == 'bar': - g = Bar(config) - # elif type == 'hbar': - # g = HorizontalBar(labels) - # elif type == 'pie': - # series = 1 - # g = Pie({'fields': labels}) - elif type == 'line': - g = Line(config) - else: - return + g = getattr(pygal, type)(config) for i in range(random.randrange(1, 10)): - values = [random_value((-max, min)[random.randrange(0, 2)], - max) for i in range(data)] + if type == 'Pie': + values = random_value(min, max) + elif type == 'XY': + values = [( + random_value((-max, min)[random.randrange(0, 2)], max), + random_value((-max, min)[random.randrange(0, 2)], max)) + for i in range(data)] + else: + values = [random_value((-max, min)[random.randrange(0, 2)], + max) for i in range(data)] g.add(random_label(), values) return Response(g.render(), mimetype='image/svg+xml') @@ -83,7 +70,7 @@ def create_app(): width, height = 600, 400 svgs = [url_for('all_svg', type=type, style=style) for style in styles - for type in ('bar', 'line')] + for type in ('Bar', 'Line', 'XY', 'Pie', 'StackedBar')] return render_template('svgs.jinja2', svgs=svgs, width=width, @@ -108,7 +95,7 @@ def create_app(): @app.route("/bigline.svg") def big_line_svg(): - g = Line(600, 400) + g = pygal.Line(600, 400) g.x_labels = ['a', 'b', 'c', 'd'] g.add('serie', [11, 50, 133, 2]) return Response(g.render(), mimetype='image/svg+xml') diff --git a/out.py b/out.py new file mode 100755 index 0000000..c71bc75 --- /dev/null +++ b/out.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +from pygal import Line, Bar, XY, Pie, StackedBar, Config +from math import cos, sin + +bar = Bar() +rng = [-3, -32, -39] +bar.add('test1', rng) +bar.add('test2', map(abs, rng)) +bar.x_labels = map(str, rng) +bar.title = "Bar test" +with open('out-bar.svg', 'w') as f: + f.write(bar.render()) + +stackedbar = StackedBar() +rng = [3, 32, 39, 12] +stackedbar.add('test1', rng) +rng2 = [24, 8, 18, 12] +stackedbar.add('test2', rng2) +rng3 = [6, 1, -10, 0] +stackedbar.add('test3', rng3) +stackedbar.x_labels = map(lambda x: '%s / %s / %s' % x, + zip(map(str, rng), + map(str, rng2), + map(str, rng3))) +stackedbar.title = "Stackedbar test" +with open('out-stackedbar.svg', 'w') as f: + f.write(stackedbar.render()) + +line = Line(Config(y_scale=.0005)) +rng = range(-30, 31, 5) +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]) +line.x_labels = map(str, rng) +line.title = "Line test" +with open('out-line.svg', 'w') as f: + f.write(line.render()) + +xy = XY(Config(x_scale=1)) +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.title = "XY test" +with open('out-xy.svg', 'w') as f: + f.write(xy.render()) + +pie = Pie() +pie.add('test', 121) +pie.add('test2', 29) +# pie.add('test3', 242) +# pie.add('test4', 90) +# pie.add('test5', 175) +pie.title = "Pie test" +with open('out-pie.svg', 'w') as f: + f.write(pie.render()) diff --git a/pygal/__init__.py b/pygal/__init__.py index 1fdcfd0..68a39a5 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -1,6 +1,7 @@ from collections import namedtuple from pygal.bar import Bar +from pygal.stackedbar import StackedBar from pygal.line import Line from pygal.xy import XY from pygal.pie import Pie diff --git a/pygal/base.py b/pygal/base.py index b3fb51f..8d9ff38 100644 --- a/pygal/base.py +++ b/pygal/base.py @@ -66,7 +66,20 @@ class BaseGraph(object): return self.svg.render() except Exception: from traceback import format_exc - return format_exc() + error_svg = ( + '' + '' + '') + trace = (format_exc() + .replace('&', '&') + .replace('<', '<') + .replace('>', '>')) + for i, line in enumerate(trace.split('\n')): + error_svg += '%s' % ( + (i + 1) * 25, line) + error_svg += '' + return error_svg def validate(self): if self.x_labels: diff --git a/pygal/stackedbar.py b/pygal/stackedbar.py new file mode 100644 index 0000000..bcfdba8 --- /dev/null +++ b/pygal/stackedbar.py @@ -0,0 +1,38 @@ +from pygal.base import BaseGraph + + +class StackedBar(BaseGraph): + """Stacked Bar graph""" + + def _draw(self): + transposed = zip(*[serie.values for serie in self.series]) + vals = [sum(val) for val in transposed] + ymin, ymax = min(min(vals), 0), max(max(vals), 0) + length = len(self.series[0].values) + x_pos = [x / float(length) for x in range(length + 1) + ] if length > 1 else [0, 1] # Center if only one value + y_pos = self._pos( + ymin, ymax, self.y_scale) if not self.y_labels else map( + int, self.y_labels) + x_ranges = zip(x_pos, x_pos[1:]) + + x_labels = self.x_labels and zip(self.x_labels, [ + sum(x_range) / 2 for x_range in x_ranges]) + y_labels = zip(map(str, y_pos), y_pos) + + self._compute_margin(x_labels, y_labels) + self.svg.set_view(ymin, ymax) + self.svg.make_graph() + self.svg.x_axis(x_labels) + self.svg.y_axis(y_labels) + self.svg.legend([serie.title for serie in self.series]) + self.svg.title() + + stack_vals = [0] * length + for serie in self.series: + serie_node = self.svg.serie(serie.index) + stack_vals = self.svg.stackbar( + serie_node, serie, [ + tuple((x_ranges[i][j], v) for j in range(2)) + for i, v in enumerate(serie.values)], + stack_vals) diff --git a/pygal/svg.py b/pygal/svg.py index 3fec3c2..d765f59 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -133,10 +133,9 @@ class Svg(object): return self.node( self.plot, class_='series serie-%d color-%d' % (serie, serie)) - def line(self, serie_node, values, origin=None): + def line(self, serie_node, values): view_values = map(self.view, values) - if origin == None: - origin = '%f %f' % view_values[0] + origin = '%f %f' % view_values[0] dots = self.node(serie_node, class_="dots") for i, (x, y) in enumerate(view_values): @@ -148,7 +147,7 @@ class Svg(object): self.node(serie_node, 'path', d='M%s L%s' % (origin, svg_values), class_='line') - def bar(self, serie_node, serie, values, origin=None): + def bar(self, serie_node, serie, values): """Draw a bar graph for a serie""" # value here is a list of tuple range of tuple coord @@ -188,26 +187,67 @@ class Svg(object): y=y_txt, ).text = str(values[i][1][1]) + def stackbar(self, serie_node, serie, values, stack_vals): + """Draw a bar graph for a serie""" + # value here is a list of tuple range of tuple coord + + def view(rng): + """Project range""" + return (self.view(rng[0]), self.view(rng[1])) + + bars = self.node(serie_node, class_="bars") + view_values = map(view, values) + for i, ((x, y), (X, Y)) in enumerate(view_values): + # x and y are left range coords and X, Y right ones + width = X - x + padding = .1 * width + inner_width = width - 2 * padding + height = self.view.y(0) - y + x = x + padding + y_txt = y + height / 2 + shift = stack_vals[i] + stack_vals[i] += height + if height < 0: + y = y + height + height = -height + y_txt = y + height / 2 + bar = self.node(bars, class_='bar') + self.node(bar, 'rect', + x=x, + y=y - shift, + rx=self.graph.rounded_bars * 1, + ry=self.graph.rounded_bars * 1, + width=inner_width, + height=height, + class_='rect') + self.node(bar, 'text', + x=x + inner_width / 2, + y=y_txt - shift, + ).text = str(values[i][1][1]) + return stack_vals + def slice(self, serie_node, start_angle, angle, perc): slices = self.node(serie_node, class_="slices") slice_ = self.node(slices, class_="slice") center = ((self.graph.width - self.graph.margin.x) / 2., (self.graph.height - self.graph.margin.y) / 2.) - r = min(center) - 20 + r = min(center) center_str = '%f %f' % center rxy = '%f %f' % tuple([r] * 2) to = '%f %f' % (r * sin(angle), r * (1 - cos(angle))) self.node(slice_, 'path', - d='M%s v%f a%s 0 0 1 %s z' % ( - center_str, -center[1] + 20, - rxy, to), + d='M%s v%f a%s 0 %d 1 %s z' % ( + center_str, -r, + rxy, + 1 if angle > pi else 0, + to), transform='rotate(%f %s)' % ( start_angle * 180 / pi, center_str), class_='slice') text_angle = pi / 2. - (start_angle + angle / 2.) text_r = min(center) self.node(slice_, 'text', - x=center[0] + text_r * cos(text_angle) * 1.05, + x=center[0] + text_r * cos(text_angle), y=center[1] - text_r * sin(text_angle), ).text = '{:.2%}'.format(perc) diff --git a/relauncher b/relauncher index 265dcc6..fb56a84 100755 --- a/relauncher +++ b/relauncher @@ -1,4 +1,5 @@ #!/bin/zsh -while inotifywait -e modify **/*.py >> ~/.log/inotifywait.log 2>&1; do - py.test pygal/test -done +livereload& +reload ./out.py& +python -m SimpleHTTPServer 1515& +chromium http://localhost:1515/&