Browse Source

Big refactor and add radar graph

pull/8/head
Florian Mounier 13 years ago
parent
commit
80f39a19c4
  1. 4
      out.py
  2. 2
      pygal/config.py
  3. 10
      pygal/css/graph.css
  4. 65
      pygal/graph/bar.py
  5. 8
      pygal/graph/base.py
  6. 128
      pygal/graph/graph.py
  7. 3
      pygal/graph/horizontal.py
  8. 22
      pygal/graph/line.py
  9. 35
      pygal/graph/pie.py
  10. 76
      pygal/graph/radar.py
  11. 8
      pygal/graph/stackedbar.py
  12. 11
      pygal/graph/xy.py
  13. 3
      pygal/style.py
  14. 251
      pygal/svg.py
  15. 4
      pygal/util.py
  16. 10
      pygal/view.py

4
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"

2
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"""

10
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 }};
}

65
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)])

8
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()

128
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))

3
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

22
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)])

35
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

76
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)])

8
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)],

11
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)

3
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,

251
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(

4
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

10
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)))

Loading…
Cancel
Save