From 80f39a19c4b7ad125dde15f75e85c0c2e49e9a0c Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Tue, 14 Feb 2012 17:59:27 +0100 Subject: [PATCH] Big refactor and add radar graph --- out.py | 4 +- pygal/config.py | 2 + pygal/css/graph.css | 10 +- pygal/graph/bar.py | 65 +++++++++- pygal/graph/base.py | 8 -- pygal/graph/graph.py | 128 +++++++++++++++++++ pygal/graph/horizontal.py | 3 +- pygal/graph/line.py | 22 +++- pygal/graph/pie.py | 35 +++++- pygal/graph/radar.py | 76 ++++++++++-- pygal/graph/stackedbar.py | 8 +- pygal/graph/xy.py | 11 +- pygal/style.py | 3 + pygal/svg.py | 251 +++----------------------------------- pygal/util.py | 4 + pygal/view.py | 10 ++ 16 files changed, 358 insertions(+), 282 deletions(-) create mode 100644 pygal/graph/graph.py diff --git a/out.py b/out.py index 3f177ed..ba63be4 100755 --- a/out.py +++ b/out.py @@ -82,10 +82,12 @@ with open('out-pie.svg', 'w') as f: f.write(pie.render()) config = Config() +config.fill = True +config.style = NeonStyle config.x_labels = ( 'black', 'red', 'blue', 'yellow', 'orange', 'green', 'white') radar = Radar(config) -radar.add('test', [9, 10, 3, 5, 7, 2, 5]) +radar.add('test', [1, 4, 1, 5, 7, 2, 5]) radar.add('test2', [10, 2, 0, 5, 1, 9, 4]) radar.title = "Radar test" diff --git a/pygal/config.py b/pygal/config.py index 3529709..6535668 100644 --- a/pygal/config.py +++ b/pygal/config.py @@ -41,6 +41,8 @@ class Config(object): rounded_bars = False # Always include x axis x_start_at_zero = False + # Fill areas + fill = False def __init__(self, **kwargs): """Can be instanciated with config kwargs""" diff --git a/pygal/css/graph.css b/pygal/css/graph.css index b926762..e5cc2c6 100644 --- a/pygal/css/graph.css +++ b/pygal/css/graph.css @@ -20,6 +20,10 @@ svg * { fill: {{ style.foreground }}; } +.line { + fill-opacity: 0; +} + .title { fill: {{ style.foreground_light }}; font-size: {{ font_sizes.title }}; @@ -53,7 +57,7 @@ svg * { text-anchor: middle; } -.axis.x text[transform] { +.axis.x:not(.web) text[transform] { text-anchor: start; } @@ -115,13 +119,13 @@ svg * { } .series .line { - fill: none; stroke-width: 1px; + fill-opacity: {{ fill_opacity }}; } .series .line:hover { - fill: none; stroke-width: 2px; + fill-opacity: {{ fill_opacity_hover }}; } diff --git a/pygal/graph/bar.py b/pygal/graph/bar.py index a5a887d..088eead 100644 --- a/pygal/graph/bar.py +++ b/pygal/graph/bar.py @@ -1,9 +1,66 @@ -from pygal.graph.base import BaseGraph +from pygal.graph.graph import Graph +from pygal.util import swap, ident -class Bar(BaseGraph): +class Bar(Graph): """Bar graph""" + def bar(self, serie_node, serie, values, stack_vals=None): + """Draw a bar graph for a serie""" + # value here is a list of tuple range of tuple coord + + def view(rng): + """Project range""" + t, T = rng + fun = swap if self.horizontal else ident + return (self.view(fun(t)), self.view(fun(T))) + + bars = self.svg.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.horizontal: + x, y, X, Y = Y, X, y, x + width = X - x + padding = .1 * width + inner_width = width - 2 * padding + if self.horizontal: + height = self.view.x(0) - y + else: + height = self.view.y(0) - y + if stack_vals == None: + bar_width = inner_width / len(self.series) + bar_padding = .1 * bar_width + bar_inner_width = bar_width - 2 * bar_padding + offset = serie.index * bar_width + bar_padding + shift = 0 + else: + offset = 0 + bar_inner_width = inner_width + shift = stack_vals[i][int(height < 0)] + stack_vals[i][int(height < 0)] += height + x = x + padding + offset + + if height < 0: + y = y + height + height = -height + + y_txt = y + height / 2 + .3 * self.values_font_size + bar = self.svg.node(bars, class_='bar') + self.svg.transposable_node(bar, 'rect', + x=x, + y=y - shift, + rx=self.rounded_bars * 1, + ry=self.rounded_bars * 1, + width=bar_inner_width, + height=height, + class_='rect') + self.svg.transposable_node(bar, 'text', + x=x + bar_inner_width / 2, + y=y_txt - shift, + ).text = str(values[i][1][1]) + return stack_vals + def _compute(self): vals = [val for serie in self.series for val in serie.values] self._box.ymin, self._box.ymax = min(min(vals), 0), max(max(vals), 0) @@ -20,7 +77,7 @@ class Bar(BaseGraph): def _plot(self): for serie in self.series: - serie_node = self.svg.serie(serie.index) - self.svg.bar(serie_node, serie, [ + serie_node = self._serie(serie.index) + self.bar(serie_node, serie, [ tuple((self._x_ranges[i][j], v) for j in range(2)) for i, v in enumerate(serie.values)]) diff --git a/pygal/graph/base.py b/pygal/graph/base.py index b53f2fc..9bcf856 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -86,14 +86,6 @@ class BaseGraph(object): def _legends(self): return [serie.title for serie in self.series] - def _decorate(self): - self.svg.set_view() - self.svg.make_graph() - self.svg.x_axis() - self.svg.y_axis() - self.svg.legend() - self.svg.title() - def _draw(self): self._compute() self._compute_margin() diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py new file mode 100644 index 0000000..4a57bc2 --- /dev/null +++ b/pygal/graph/graph.py @@ -0,0 +1,128 @@ +from pygal.graph.base import BaseGraph +from pygal.view import View + + +class Graph(BaseGraph): + """Graph super class containing generic common functions""" + + def _decorate(self): + self._set_view() + self._make_graph() + self._x_axis() + self._y_axis() + self._legend() + self._title() + + def _set_view(self): + self.view = View( + self.width - self.margin.x, + self.height - self.margin.y, + self._box) + + def _make_graph(self): + self.graph_node = self.svg.node( + class_='graph %s' % self.__class__.__name__) + self.svg.node(self.graph_node, 'rect', + class_='background', + x=0, y=0, + width=self.width, + height=self.height) + self.plot = self.svg.node( + self.graph_node, class_="plot", + transform="translate(%d, %d)" % ( + self.margin.left, self.margin.top)) + self.svg.node(self.plot, 'rect', + class_='background', + x=0, y=0, + width=self.view.width, + height=self.view.height) + + def _x_axis(self): + if not self._x_labels: + return + + axis = self.svg.node(self.plot, class_="axis x") + + if 0 not in [label[1] for label in self._x_labels]: + self.svg.node(axis, 'path', + d='M%f %f v%f' % (0, 0, self.view.height), + class_='line') + for label, position in self._x_labels: + guides = self.svg.node(axis, class_='guides') + x = self.view.x(position) + y = self.view.height + 5 + self.svg.node(guides, 'path', + d='M%f %f v%f' % (x, 0, self.view.height), + class_='%sline' % ( + 'guide ' if position != 0 else '')) + text = self.svg.node(guides, 'text', + x=x, + y=y + .5 * self.label_font_size + 5 + ) + text.text = label + if self.x_label_rotation: + text.attrib['transform'] = "rotate(%d %f %f)" % ( + self.x_label_rotation, x, y) + + def _y_axis(self): + if not self._y_labels: + return + + axis = self.svg.node(self.plot, class_="axis y") + + if 0 not in [label[1] for label in self._y_labels]: + self.svg.node(axis, 'path', + d='M%f %f h%f' % (0, self.view.height, self.view.width), + class_='line') + for label, position in self._y_labels: + guides = self.svg.node(axis, class_='guides') + x = -5 + y = self.view.y(position) + self.svg.node(guides, 'path', + d='M%f %f h%f' % (0, y, self.view.width), + class_='%sline' % ( + 'guide ' if position != 0 else '')) + text = self.svg.node(guides, 'text', + x=x, + y=y + .35 * self.label_font_size + ) + text.text = label + if self.y_label_rotation: + text.attrib['transform'] = "rotate(%d %f %f)" % ( + self.y_label_rotation, x, y) + + def _legend(self): + if not self.show_legend: + return + legends = self.svg.node( + self.graph_node, class_='legends', + transform='translate(%d, %d)' % ( + self.margin.left + self.view.width + 10, + self.margin.top + 10)) + for i, title in enumerate(self._legends): + legend = self.svg.node(legends, class_='legend') + self.svg.node(legend, 'rect', + x=0, + y=1.5 * i * self.legend_box_size, + width=self.legend_box_size, + height=self.legend_box_size, + class_="color-%d" % i, + ).text = title + # Serious magical numbers here + self.svg.node(legend, 'text', + x=self.legend_box_size + 5, + y=1.5 * i * self.legend_box_size + + .5 * self.legend_box_size + + .3 * self.legend_font_size + ).text = title + + def _title(self): + if self.title: + self.svg.node(self.graph_node, 'text', class_='title', + x=self.margin.left + self.view.width / 2, + y=self.title_font_size + 10 + ).text = self.title + + def _serie(self, serie): + return self.svg.node( + self.plot, class_='series serie-%d color-%d' % (serie, serie)) diff --git a/pygal/graph/horizontal.py b/pygal/graph/horizontal.py index 83a791c..bb4b5c0 100644 --- a/pygal/graph/horizontal.py +++ b/pygal/graph/horizontal.py @@ -1,8 +1,9 @@ +from pygal.graph.graph import Graph from pygal.graph.bar import Bar from pygal.graph.stackedbar import StackedBar -class HorizontalGraph(object): +class HorizontalGraph(Graph): """Horizontal graph""" def __init__(self, *args, **kwargs): kwargs['horizontal'] = True diff --git a/pygal/graph/line.py b/pygal/graph/line.py index 9d78f13..3f1d1d7 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -1,9 +1,23 @@ -from pygal.graph.base import BaseGraph +from pygal.graph.graph import Graph -class Line(BaseGraph): +class Line(Graph): """Line graph""" + def _get_value(self, values, i): + return str(values[i][1]) + + def line(self, serie_node, values): + view_values = map(self.view, values) + + 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) + self.svg.line(serie_node, view_values, class_='line', close=True) + def _compute(self): vals = [val for serie in self.series for val in serie.values] self._box.ymin, self._box.ymax = min(vals), max(vals) @@ -18,7 +32,7 @@ class Line(BaseGraph): def _plot(self): for serie in self.series: - self.svg.line( - self.svg.serie(serie.index), [ + self.line( + self._serie(serie.index), [ (self._x_pos[i], v) for i, v in enumerate(serie.values)]) diff --git a/pygal/graph/pie.py b/pygal/graph/pie.py index f095364..f3b0a84 100644 --- a/pygal/graph/pie.py +++ b/pygal/graph/pie.py @@ -1,11 +1,36 @@ from pygal.serie import Serie -from pygal.graph.base import BaseGraph -from math import pi +from pygal.graph.graph import Graph +from math import cos, sin, pi -class Pie(BaseGraph): +class Pie(Graph): """Pie graph""" + def slice(self, serie_node, start_angle, angle, perc): + slices = self.svg.node(serie_node, class_="slices") + slice_ = self.svg.node(slices, class_="slice") + center = ((self.width - self.margin.x) / 2., + (self.height - self.margin.y) / 2.) + 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.svg.node(slice_, 'path', + 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) * .8 + self.svg.node(slice_, 'text', + x=center[0] + text_r * cos(text_angle), + 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))) @@ -15,8 +40,8 @@ class Pie(BaseGraph): for serie in self.series: val = serie.values[0] angle = 2 * pi * val / total - self.svg.slice( - self.svg.serie(serie.index), + self.slice( + self._serie(serie.index), current_angle, angle, val / total) current_angle += angle diff --git a/pygal/graph/radar.py b/pygal/graph/radar.py index a97a818..159111f 100644 --- a/pygal/graph/radar.py +++ b/pygal/graph/radar.py @@ -1,27 +1,81 @@ -from pygal.graph.base import BaseGraph -from math import pi +from pygal.graph.line import Line +from pygal.view import PolarView +from pygal.util import deg +from math import cos, sin, pi -class Radar(BaseGraph): +class Radar(Line): """Kiviat graph""" + def _set_view(self): + self.view = PolarView( + self.width - self.margin.x, + self.height - self.margin.y, + self._box) + + def _x_axis(self): + if not self._x_labels: + return + + axis = self.svg.node(self.plot, class_="axis x web") + format = lambda x: '%f %f' % x + center = self.view((0, 0)) + r = self._rmax + for label, theta in self._x_labels: + guides = self.svg.node(axis, class_='guides') + end = self.view((r, theta)) + self.svg.node(guides, 'path', + d='M%s L%s' % (format(center), format(end)), + class_='line') + r_txt = (1 - self._box.__class__._margin) * self._box.ymax + pos_text = self.view((r_txt, theta)) + text = self.svg.node(guides, 'text', + x=pos_text[0], + y=pos_text[1] + ) + text.text = label + angle = - theta + pi / 2. + if cos(angle) < 0: + angle -= pi + text.attrib['transform'] = 'rotate(%f %s)' % ( + deg(angle), format(pos_text)) + + def _y_axis(self): + if not self._y_labels: + return + + axis = self.svg.node(self.plot, class_="axis y web") + + for label, r in reversed(self._y_labels): + guides = self.svg.node(axis, class_='guides') + self.svg.line( + guides, [self.view((r, theta)) for theta in self._x_pos], + close=True, + class_='guide line') + x, y = self.view((r, self._x_pos[0])) + self.svg.node(guides, 'text', + x=x - 5, + y=y + ).text = label + def _compute(self): vals = [val for serie in self.series for val in serie.values] - self._box.ymax = 2 * max(vals) - self._box.ymin = - self._box.ymax - self._box.xmin = self._box.ymin - self._box.xmax = self._box.ymax + self._box._margin *= 2 + self._box.xmin = self._box.ymin = 0 + self._box.xmax = self._box.ymax = self._rmax = max(vals) - delta = 2 * pi / float(len(self.x_labels)) 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 ) 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 def _plot(self): for serie in self.series: - serie_node = self.svg.serie(serie.index) - # self.svg.web(serie_node, serie, - # [val / float(self._rmax) for val in serie.values]) + serie_node = self._serie(serie.index) + self.line(serie_node, [ + (v, self._x_pos[i]) + for i, v in enumerate(serie.values)]) diff --git a/pygal/graph/stackedbar.py b/pygal/graph/stackedbar.py index 5329231..51bba91 100644 --- a/pygal/graph/stackedbar.py +++ b/pygal/graph/stackedbar.py @@ -1,7 +1,7 @@ -from pygal.graph.base import BaseGraph +from pygal.graph.bar import Bar -class StackedBar(BaseGraph): +class StackedBar(Bar): """Stacked Bar graph""" def _compute(self): @@ -29,8 +29,8 @@ class StackedBar(BaseGraph): def _plot(self): stack_vals = [[0, 0] for i in range(self._length)] for serie in self.series: - serie_node = self.svg.serie(serie.index) - stack_vals = self.svg.bar( + serie_node = self._serie(serie.index) + stack_vals = self.bar( serie_node, serie, [ tuple((self._x_ranges[i][j], v) for j in range(2)) for i, v in enumerate(serie.values)], diff --git a/pygal/graph/xy.py b/pygal/graph/xy.py index 406308d..f72083c 100644 --- a/pygal/graph/xy.py +++ b/pygal/graph/xy.py @@ -1,9 +1,12 @@ -from pygal.graph.base import BaseGraph +from pygal.graph.line import Line -class XY(BaseGraph): +class XY(Line): """XY Line graph""" + def _get_value(self, values, i): + return str(values[i]) + def _compute(self): for serie in self.series: serie.values = sorted(serie.values, key=lambda x: x[0]) @@ -20,5 +23,5 @@ class XY(BaseGraph): def _plot(self): for serie in self.series: - self.svg.line( - self.svg.serie(serie.index), serie.values, True) + self.line( + self._serie(serie.index), serie.values) diff --git a/pygal/style.py b/pygal/style.py index c2c279c..1991da8 100644 --- a/pygal/style.py +++ b/pygal/style.py @@ -6,6 +6,7 @@ class Style(object): foreground_light='#eee', foreground_dark='#555', opacity='.8', + opacity_hover='1', transition='250ms', colors=( '#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe', @@ -18,6 +19,7 @@ class Style(object): self.foreground_light = foreground_light self.foreground_dark = foreground_dark self.opacity = opacity + self.opacity_hover = opacity_hover self.transition = transition self._colors = colors @@ -43,6 +45,7 @@ LightStyle = Style( '#77bbb5', '#777777')) NeonStyle = Style( opacity='.1', + opacity_hover='.75', transition='1s ease-out') styles = {'default': DefaultStyle, diff --git a/pygal/svg.py b/pygal/svg.py index 2820ac8..41f4363 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -1,8 +1,7 @@ # -*- coding: utf-8 -*- import os from lxml import etree -from pygal.view import View -from pygal.util import template, swap, ident +from pygal.util import template from math import cos, sin, pi @@ -34,7 +33,11 @@ class Svg(object): f.read(), style=self.graph.style, font_sizes=self.graph.font_sizes, - hidden='y' if self.graph.horizontal else 'x') + hidden='y' if self.graph.horizontal else 'x', + fill_opacity=self.graph.style.opacity + if self.graph.fill else 0, + fill_opacity_hover=self.graph.style.opacity_hover + if self.graph.fill else 0) def node(self, parent=None, tag='g', attrib=None, **extras): if parent is None: @@ -59,241 +62,15 @@ class Svg(object): extras[key1], extras[key2] = attr2, attr1 return self.node(parent, tag, attrib, **extras) - def set_view(self): - self.view = View( - self.graph.width - self.graph.margin.x, - self.graph.height - self.graph.margin.y, - self.graph._box) + def format(self, xy): + return '%f %f' % xy - def make_graph(self): - self.graph_node = self.node( - class_='graph %s' % self.graph.__class__.__name__) - self.node(self.graph_node, 'rect', - class_='background', - x=0, y=0, - width=self.graph.width, - height=self.graph.height) - self.plot = self.node( - self.graph_node, class_="plot", - transform="translate(%d, %d)" % ( - self.graph.margin.left, self.graph.margin.top)) - self.node(self.plot, 'rect', - class_='background', - x=0, y=0, - width=self.view.width, - height=self.view.height) - - def x_axis(self): - if not self.graph._x_labels: - return - - axis = self.node(self.plot, class_="axis x") - - if 0 not in [label[1] for label in self.graph._x_labels]: - self.node(axis, 'path', - d='M%f %f v%f' % (0, 0, self.view.height), - class_='line') - for label, position in self.graph._x_labels: - guides = self.node(axis, class_='guides') - x = self.view.x(position) - y = self.view.height + 5 - self.node(guides, 'path', - d='M%f %f v%f' % (x, 0, self.view.height), - class_='%sline' % ( - 'guide ' if position != 0 else '')) - text = self.node(guides, 'text', - x=x, - y=y + .5 * self.graph.label_font_size + 5) - if self.graph.x_label_rotation: - text.attrib['transform'] = "rotate(%d %f %f)" % ( - self.graph.x_label_rotation, x, y) - text.text = label - - def y_axis(self): - if not self.graph._y_labels: - return - - axis = self.node(self.plot, class_="axis y") - - if 0 not in [label[1] for label in self.graph._y_labels]: - self.node(axis, 'path', - d='M%f %f h%f' % (0, self.view.height, self.view.width), - class_='line') - for label, position in self.graph._y_labels: - guides = self.node(axis, class_='guides') - x = -5 - y = self.view.y(position) - self.node(guides, 'path', - d='M%f %f h%f' % (0, y, self.view.width), - class_='%sline' % ( - 'guide ' if position != 0 else '')) - text = self.node(guides, 'text', - x=x, - y=y + .35 * self.graph.label_font_size) - if self.graph.y_label_rotation: - text.attrib['transform'] = "rotate(%d %f %f)" % ( - self.graph.y_label_rotation, x, y) - text.text = label - - def web_axis(self): - axis = self.node(self.plot, class_="axis polar") - delta = 2 * pi / float(len(self.graph.x_labels)) - center = self.view((0, 0)) - f = lambda x: '%f %f' % x - for i, title in enumerate(self.graph.x_labels): - angle = .5 * pi - i * delta - end = self.view((cos(angle), sin(angle))) - self.node(axis, 'path', - d='M%s L%s' % (f(center), f(end)), - class_='line') - self.node(axis, 'text', - x=end[0], - y=end[1] - ).text = str(i) - - def legend(self): - if not self.graph.show_legend: - return - legends = self.node( - self.graph_node, class_='legends', - transform='translate(%d, %d)' % ( - self.graph.margin.left + self.view.width + 10, - self.graph.margin.top + 10)) - for i, title in enumerate(self.graph._legends): - legend = self.node(legends, class_='legend') - self.node(legend, 'rect', - x=0, - y=1.5 * i * self.graph.legend_box_size, - width=self.graph.legend_box_size, - height=self.graph.legend_box_size, - class_="color-%d" % i, - ).text = title - # Serious magical numbers here - self.node(legend, 'text', - x=self.graph.legend_box_size + 5, - y=1.5 * i * self.graph.legend_box_size - + .5 * self.graph.legend_box_size - + .3 * self.graph.legend_font_size - ).text = title - - def title(self): - if self.graph.title: - self.node(self.graph_node, 'text', class_='title', - x=self.graph.margin.left + self.view.width / 2, - y=self.graph.title_font_size + 10 - ).text = self.graph.title - - def serie(self, serie): - return self.node( - self.plot, class_='series serie-%d color-%d' % (serie, serie)) - - def line(self, serie_node, values, xy=False): - view_values = map(self.view, values) - origin = '%f %f' % view_values[0] - - dots = self.node(serie_node, class_="dots") - for i, (x, y) in enumerate(view_values): - dot = self.node(dots, class_='dot') - self.node(dot, 'circle', cx=x, cy=y, r=2.5) - self.node(dot, 'text', x=x, y=y).text = str( - values[i]) if xy else str(values[i][1]) - - svg_values = ' '.join(map(lambda x: '%f %f' % x, view_values)) - self.node(serie_node, 'path', - d='M%s L%s' % (origin, svg_values), class_='line') - - def bar(self, serie_node, serie, values, stack_vals=None): - """Draw a bar graph for a serie""" - # value here is a list of tuple range of tuple coord - - def view(rng): - """Project range""" - 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 - 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 - bar_inner_width = bar_width - 2 * bar_padding - offset = serie.index * bar_width + bar_padding - shift = 0 - else: - offset = 0 - bar_inner_width = inner_width - shift = stack_vals[i][int(height < 0)] - stack_vals[i][int(height < 0)] += height - x = x + padding + offset - - if height < 0: - y = y + height - height = -height - - y_txt = y + height / 2 + .3 * self.graph.values_font_size - bar = self.node(bars, class_='bar') - self.transposable_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.transposable_node(bar, 'text', - x=x + bar_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) - 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 %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) * .8 - self.node(slice_, 'text', - x=center[0] + text_r * cos(text_angle), - y=center[1] - text_r * sin(text_angle), - ).text = '{:.2%}'.format(perc) - - def web(self, serie_node, serie, radius): - webs = self.node(serie_node, class_="webs") - web = self.node(webs, class_="web") - # view_radius = map(self.view, radius) - origin = '%f %f' % self.view((0, 0)) - dot1 = '%f %f' % self.view((1, 1)) - dot2 = '%f %f' % self.view((-1, -1)) - self.node(web, 'path', - d='M%s L%s %s' % ( - origin, dot1, dot2), - class_='web') + 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:])) + self.node(node, 'path', + d=root % (origin, line), **kwargs) def render(self): return etree.tostring( diff --git a/pygal/util.py b/pygal/util.py index 7007e3d..2699a44 100644 --- a/pygal/util.py +++ b/pygal/util.py @@ -32,6 +32,10 @@ def rad(deg): return pi * deg / 180. +def deg(deg): + return 180 * deg / pi + + def _swap_curly(string): """Swap single and double curly brackets""" return (string diff --git a/pygal/view.py b/pygal/view.py index 1eee897..1060370 100644 --- a/pygal/view.py +++ b/pygal/view.py @@ -1,3 +1,6 @@ +from math import sin, cos + + class Margin(object): def __init__(self, top, right, bottom, left): self.top = top @@ -64,3 +67,10 @@ class View(object): def __call__(self, xy): x, y = xy return (self.x(x), self.y(y)) + + +class PolarView(View): + def __call__(self, rtheta): + r, theta = rtheta + return super(PolarView, self).__call__( + (r * cos(theta), r * sin(theta)))