diff --git a/demo/moulinrouge/__init__.py b/demo/moulinrouge/__init__.py index b5c25fa..6bf99e2 100644 --- a/demo/moulinrouge/__init__.py +++ b/demo/moulinrouge/__init__.py @@ -70,7 +70,8 @@ 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', 'XY', 'Pie', 'StackedBar')] + for type in ('Bar', 'Line', 'XY', 'Pie', 'StackedBar', + 'HorizontalBar', 'HorizontalStackedBar')] return render_template('svgs.jinja2', svgs=svgs, width=width, diff --git a/out.py b/out.py index b1ce739..94861eb 100755 --- a/out.py +++ b/out.py @@ -1,5 +1,7 @@ #!/usr/bin/env python -from pygal import Line, Bar, XY, Pie, StackedBar, Config +from pygal import ( + Line, Bar, XY, Pie, StackedBar, Config, + HorizontalBar, HorizontalStackedBar) from pygal.style import NeonStyle from math import cos, sin @@ -12,6 +14,17 @@ bar.title = "Bar test" with open('out-bar.svg', 'w') as f: f.write(bar.render()) +hbar = HorizontalBar() +rng = [18, 9, 7, 3, 1, 0, -5] +hbar.add('test1', rng) +rng2 = [16, 14, 10, 9, 7, 3, -1] +hbar.add('test2', rng2) +hbar.x_labels = map( + lambda x: '%s / %s' % x, zip(map(str, rng), map(str, rng2))) +hbar.title = "Horizontal Bar test" +with open('out-horizontalbar.svg', 'w') as f: + f.write(hbar.render()) + rng = [3, -32, 39, 12] rng2 = [24, -8, 18, 12] @@ -22,8 +35,9 @@ config.x_labels = map(lambda x: '%s / %s / %s' % x, zip(map(str, rng), map(str, rng2), map(str, rng3))) -config.title = "Config test" +config.title = "Stacked Bar test" config.style = NeonStyle +config.horizontal = True stackedbar = StackedBar(config) stackedbar.add('@@@@@@@', rng) @@ -32,6 +46,14 @@ stackedbar.add('--->', rng3) with open('out-stackedbar.svg', 'w') as f: f.write(stackedbar.render()) +config.title = "Horizontal Stacked Bar test" +hstackedbar = HorizontalStackedBar(config) +hstackedbar.add('@@@@@@@', rng) +hstackedbar.add('++++++', rng2) +hstackedbar.add('--->', rng3) +with open('out-horizontalstackedbar.svg', 'w') as f: + f.write(hstackedbar.render()) + line = Line(Config(y_scale=.0005)) rng = range(-30, 31, 5) line.add('test1', [cos(x / 10.) for x in rng]) diff --git a/pygal/__init__.py b/pygal/__init__.py index b5a5c94..4c4232d 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -1,7 +1,9 @@ from collections import namedtuple from pygal.graph.bar import Bar +from pygal.graph.horizontal import HorizontalBar from pygal.graph.stackedbar import StackedBar +from pygal.graph.horizontal import HorizontalStackedBar from pygal.graph.line import Line from pygal.graph.xy import XY from pygal.graph.pie import Pie diff --git a/pygal/css/graph.css b/pygal/css/graph.css index c6e6006..b926762 100644 --- a/pygal/css/graph.css +++ b/pygal/css/graph.css @@ -71,7 +71,7 @@ svg * { stroke-dasharray: 5,5; } -.axis.x .guide.line { +.axis.{{ hidden }} .guide.line { opacity: 0; } diff --git a/pygal/graph/base.py b/pygal/graph/base.py index 0b229e2..b53f2fc 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -3,13 +3,14 @@ from pygal.view import Margin, Box from pygal.util import round_to_scale, cut, rad from pygal.svg import Svg from pygal.config import Config -from math import log10, sin, cos, pi +from math import log10, sin, cos class BaseGraph(object): """Graphs commons""" - def __init__(self, config=None): + def __init__(self, config=None, horizontal=False): + self.horizontal = horizontal self.config = config or Config() self.svg = Svg(self) self.series = [] diff --git a/pygal/graph/horizontal.py b/pygal/graph/horizontal.py new file mode 100644 index 0000000..83a791c --- /dev/null +++ b/pygal/graph/horizontal.py @@ -0,0 +1,25 @@ +from pygal.graph.bar import Bar +from pygal.graph.stackedbar import StackedBar + + +class HorizontalGraph(object): + """Horizontal graph""" + def __init__(self, *args, **kwargs): + kwargs['horizontal'] = True + super(HorizontalGraph, self).__init__(*args, **kwargs) + + def _compute(self): + super(HorizontalGraph, self)._compute() + self._x_labels, self._y_labels = self._y_labels, self._x_labels + self._box.swap() + # Y axis is inverted + for serie in self.series: + serie.values = reversed(serie.values) + + +class HorizontalBar(HorizontalGraph, Bar): + """Horizontal Bar graph""" + + +class HorizontalStackedBar(HorizontalGraph, StackedBar): + """Horizontal Stacked Bar graph""" diff --git a/pygal/svg.py b/pygal/svg.py index 9393f1f..e285568 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -1,7 +1,7 @@ import os from lxml import etree from pygal.view import View -from pygal.util import template +from pygal.util import template, swap, ident from math import cos, sin, pi @@ -31,7 +31,8 @@ class Svg(object): style.text = template( f.read(), style=self.graph.style, - font_sizes=self.graph.font_sizes) + font_sizes=self.graph.font_sizes, + hidden='y' if self.graph.horizontal else 'x') def node(self, parent=None, tag='g', attrib=None, **extras): if parent is None: @@ -180,16 +181,23 @@ class Svg(object): def view(rng): """Project range""" - return (self.view(rng[0]), self.view(rng[1])) + t, T = rng + fun = swap if self.graph.horizontal else ident + return (self.view(fun(t)), self.view(fun(T))) 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 + if self.graph.horizontal: + x, y, X, Y = Y, X, y, x width = X - x padding = .1 * width inner_width = width - 2 * padding - height = self.view.y(0) - y + if self.graph.horizontal: + height = self.view.x(0) - y + else: + height = self.view.y(0) - y if stack_vals == None: bar_width = inner_width / len(self.graph.series) bar_padding = .1 * bar_width @@ -209,17 +217,31 @@ class Svg(object): y_txt = y + height / 2 + .3 * self.graph.values_font_size 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=bar_inner_width, - height=height, - class_='rect') - self.node(bar, 'text', - x=x + bar_inner_width / 2, - y=y_txt - shift, + if self.graph.horizontal: + self.node(bar, 'rect', + x=y - shift, + y=x, + rx=self.graph.rounded_bars * 1, + ry=self.graph.rounded_bars * 1, + width=height, + height=bar_inner_width, + class_='rect') + self.node(bar, 'text', + x=y_txt - shift, + y=x + bar_inner_width / 2, + ).text = str(values[i][1][1]) + else: + self.node(bar, 'rect', + x=x, + y=y - shift, + rx=self.graph.rounded_bars * 1, + ry=self.graph.rounded_bars * 1, + width=bar_inner_width, + height=height, + class_='rect') + self.node(bar, 'text', + x=x + bar_inner_width / 2, + y=y_txt - shift, ).text = str(values[i][1][1]) return stack_vals diff --git a/pygal/util.py b/pygal/util.py index f628cbd..7007e3d 100644 --- a/pygal/util.py +++ b/pygal/util.py @@ -48,3 +48,6 @@ def _swap_curly(string): def template(string, **kwargs): """Format a string using double braces""" return _swap_curly(string).format(**kwargs) + +swap = lambda tuple_: tuple(reversed(tuple_)) +ident = lambda x: x diff --git a/pygal/view.py b/pygal/view.py index 20689df..008e662 100644 --- a/pygal/view.py +++ b/pygal/view.py @@ -27,6 +27,10 @@ class Box(object): def height(self): return self.ymax - self.ymin + def swap(self): + self.xmin, self.ymin = self.ymin, self.xmin + self.xmax, self.ymax = self.ymax, self.xmax + def fix(self): if not self.width: self.xmax = self.xmin + 1