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()) f.write(pie.render())
config = Config() config = Config()
config.fill = True
config.style = NeonStyle
config.x_labels = ( config.x_labels = (
'black', 'red', 'blue', 'yellow', 'orange', 'green', 'white') 'black', 'red', 'blue', 'yellow', 'orange', 'green', 'white')
radar = Radar(config) 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.add('test2', [10, 2, 0, 5, 1, 9, 4])
radar.title = "Radar test" radar.title = "Radar test"

2
pygal/config.py

@ -41,6 +41,8 @@ class Config(object):
rounded_bars = False rounded_bars = False
# Always include x axis # Always include x axis
x_start_at_zero = False x_start_at_zero = False
# Fill areas
fill = False
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Can be instanciated with config kwargs""" """Can be instanciated with config kwargs"""

10
pygal/css/graph.css

@ -20,6 +20,10 @@ svg * {
fill: {{ style.foreground }}; fill: {{ style.foreground }};
} }
.line {
fill-opacity: 0;
}
.title { .title {
fill: {{ style.foreground_light }}; fill: {{ style.foreground_light }};
font-size: {{ font_sizes.title }}; font-size: {{ font_sizes.title }};
@ -53,7 +57,7 @@ svg * {
text-anchor: middle; text-anchor: middle;
} }
.axis.x text[transform] { .axis.x:not(.web) text[transform] {
text-anchor: start; text-anchor: start;
} }
@ -115,13 +119,13 @@ svg * {
} }
.series .line { .series .line {
fill: none;
stroke-width: 1px; stroke-width: 1px;
fill-opacity: {{ fill_opacity }};
} }
.series .line:hover { .series .line:hover {
fill: none;
stroke-width: 2px; 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""" """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): def _compute(self):
vals = [val for serie in self.series for val in serie.values] 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) self._box.ymin, self._box.ymax = min(min(vals), 0), max(max(vals), 0)
@ -20,7 +77,7 @@ class Bar(BaseGraph):
def _plot(self): def _plot(self):
for serie in self.series: for serie in self.series:
serie_node = self.svg.serie(serie.index) serie_node = self._serie(serie.index)
self.svg.bar(serie_node, serie, [ self.bar(serie_node, serie, [
tuple((self._x_ranges[i][j], v) for j in range(2)) tuple((self._x_ranges[i][j], v) for j in range(2))
for i, v in enumerate(serie.values)]) for i, v in enumerate(serie.values)])

8
pygal/graph/base.py

@ -86,14 +86,6 @@ class BaseGraph(object):
def _legends(self): def _legends(self):
return [serie.title for serie in self.series] 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): def _draw(self):
self._compute() self._compute()
self._compute_margin() 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.bar import Bar
from pygal.graph.stackedbar import StackedBar from pygal.graph.stackedbar import StackedBar
class HorizontalGraph(object): class HorizontalGraph(Graph):
"""Horizontal graph""" """Horizontal graph"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
kwargs['horizontal'] = True 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""" """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): def _compute(self):
vals = [val for serie in self.series for val in serie.values] vals = [val for serie in self.series for val in serie.values]
self._box.ymin, self._box.ymax = min(vals), max(vals) self._box.ymin, self._box.ymax = min(vals), max(vals)
@ -18,7 +32,7 @@ class Line(BaseGraph):
def _plot(self): def _plot(self):
for serie in self.series: for serie in self.series:
self.svg.line( self.line(
self.svg.serie(serie.index), [ self._serie(serie.index), [
(self._x_pos[i], v) (self._x_pos[i], v)
for i, v in enumerate(serie.values)]) for i, v in enumerate(serie.values)])

35
pygal/graph/pie.py

@ -1,11 +1,36 @@
from pygal.serie import Serie from pygal.serie import Serie
from pygal.graph.base import BaseGraph from pygal.graph.graph import Graph
from math import pi from math import cos, sin, pi
class Pie(BaseGraph): class Pie(Graph):
"""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): def add(self, title, value):
self.series.append(Serie(title, [value], len(self.series))) self.series.append(Serie(title, [value], len(self.series)))
@ -15,8 +40,8 @@ class Pie(BaseGraph):
for serie in self.series: for serie in self.series:
val = serie.values[0] val = serie.values[0]
angle = 2 * pi * val / total angle = 2 * pi * val / total
self.svg.slice( self.slice(
self.svg.serie(serie.index), self._serie(serie.index),
current_angle, current_angle,
angle, val / total) angle, val / total)
current_angle += angle current_angle += angle

76
pygal/graph/radar.py

@ -1,27 +1,81 @@
from pygal.graph.base import BaseGraph from pygal.graph.line import Line
from math import pi from pygal.view import PolarView
from pygal.util import deg
from math import cos, sin, pi
class Radar(BaseGraph): class Radar(Line):
"""Kiviat graph""" """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): def _compute(self):
vals = [val for serie in self.series for val in serie.values] vals = [val for serie in self.series for val in serie.values]
self._box.ymax = 2 * max(vals) self._box._margin *= 2
self._box.ymin = - self._box.ymax self._box.xmin = self._box.ymin = 0
self._box.xmin = self._box.ymin self._box.xmax = self._box.ymax = self._rmax = max(vals)
self._box.xmax = self._box.ymax
delta = 2 * pi / float(len(self.x_labels))
x_step = len(self.series[0].values) 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._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 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) ) 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._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._y_labels = zip(map(str, self._y_pos), self._y_pos)
self._box.xmin = self._box.ymin = - self._box.ymax
def _plot(self): def _plot(self):
for serie in self.series: for serie in self.series:
serie_node = self.svg.serie(serie.index) serie_node = self._serie(serie.index)
# self.svg.web(serie_node, serie, self.line(serie_node, [
# [val / float(self._rmax) for val in serie.values]) (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""" """Stacked Bar graph"""
def _compute(self): def _compute(self):
@ -29,8 +29,8 @@ class StackedBar(BaseGraph):
def _plot(self): def _plot(self):
stack_vals = [[0, 0] for i in range(self._length)] stack_vals = [[0, 0] for i in range(self._length)]
for serie in self.series: for serie in self.series:
serie_node = self.svg.serie(serie.index) serie_node = self._serie(serie.index)
stack_vals = self.svg.bar( stack_vals = self.bar(
serie_node, serie, [ serie_node, serie, [
tuple((self._x_ranges[i][j], v) for j in range(2)) tuple((self._x_ranges[i][j], v) for j in range(2))
for i, v in enumerate(serie.values)], 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""" """XY Line graph"""
def _get_value(self, values, i):
return str(values[i])
def _compute(self): def _compute(self):
for serie in self.series: for serie in self.series:
serie.values = sorted(serie.values, key=lambda x: x[0]) serie.values = sorted(serie.values, key=lambda x: x[0])
@ -20,5 +23,5 @@ class XY(BaseGraph):
def _plot(self): def _plot(self):
for serie in self.series: for serie in self.series:
self.svg.line( self.line(
self.svg.serie(serie.index), serie.values, True) self._serie(serie.index), serie.values)

3
pygal/style.py

@ -6,6 +6,7 @@ class Style(object):
foreground_light='#eee', foreground_light='#eee',
foreground_dark='#555', foreground_dark='#555',
opacity='.8', opacity='.8',
opacity_hover='1',
transition='250ms', transition='250ms',
colors=( colors=(
'#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe', '#ff5995', '#b6e354', '#feed6c', '#8cedff', '#9e6ffe',
@ -18,6 +19,7 @@ class Style(object):
self.foreground_light = foreground_light self.foreground_light = foreground_light
self.foreground_dark = foreground_dark self.foreground_dark = foreground_dark
self.opacity = opacity self.opacity = opacity
self.opacity_hover = opacity_hover
self.transition = transition self.transition = transition
self._colors = colors self._colors = colors
@ -43,6 +45,7 @@ LightStyle = Style(
'#77bbb5', '#777777')) '#77bbb5', '#777777'))
NeonStyle = Style( NeonStyle = Style(
opacity='.1', opacity='.1',
opacity_hover='.75',
transition='1s ease-out') transition='1s ease-out')
styles = {'default': DefaultStyle, styles = {'default': DefaultStyle,

251
pygal/svg.py

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os import os
from lxml import etree 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 from math import cos, sin, pi
@ -34,7 +33,11 @@ class Svg(object):
f.read(), f.read(),
style=self.graph.style, style=self.graph.style,
font_sizes=self.graph.font_sizes, 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): def node(self, parent=None, tag='g', attrib=None, **extras):
if parent is None: if parent is None:
@ -59,241 +62,15 @@ class Svg(object):
extras[key1], extras[key2] = attr2, attr1 extras[key1], extras[key2] = attr2, attr1
return self.node(parent, tag, attrib, **extras) return self.node(parent, tag, attrib, **extras)
def set_view(self): def format(self, xy):
self.view = View( return '%f %f' % xy
self.graph.width - self.graph.margin.x,
self.graph.height - self.graph.margin.y,
self.graph._box)
def make_graph(self): def line(self, node, coords, close=False, **kwargs):
self.graph_node = self.node( root = 'M%s L%s Z' if close else 'M%s L%s'
class_='graph %s' % self.graph.__class__.__name__) origin = self.format(coords[0])
self.node(self.graph_node, 'rect', line = ' '.join(map(self.format, coords[1:]))
class_='background', self.node(node, 'path',
x=0, y=0, d=root % (origin, line), **kwargs)
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 render(self): def render(self):
return etree.tostring( return etree.tostring(

4
pygal/util.py

@ -32,6 +32,10 @@ def rad(deg):
return pi * deg / 180. return pi * deg / 180.
def deg(deg):
return 180 * deg / pi
def _swap_curly(string): def _swap_curly(string):
"""Swap single and double curly brackets""" """Swap single and double curly brackets"""
return (string return (string

10
pygal/view.py

@ -1,3 +1,6 @@
from math import sin, cos
class Margin(object): class Margin(object):
def __init__(self, top, right, bottom, left): def __init__(self, top, right, bottom, left):
self.top = top self.top = top
@ -64,3 +67,10 @@ class View(object):
def __call__(self, xy): def __call__(self, xy):
x, y = xy x, y = xy
return (self.x(x), self.y(y)) 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