Browse Source

Add pie and xy

pull/8/head
Florian Mounier 13 years ago
parent
commit
69edbfc527
  1. 23
      pygal/__init__.py
  2. 3
      pygal/bar.py
  3. 44
      pygal/base.py
  4. 22
      pygal/config.py
  5. 39
      pygal/css/graph.css
  6. 3
      pygal/line.py
  7. 27
      pygal/pie.py
  8. 11
      pygal/serie.py
  9. 50
      pygal/svg.py
  10. 14
      pygal/view.py
  11. 31
      pygal/xy.py

23
pygal/__init__.py

@ -1,20 +1,7 @@
from collections import namedtuple
Serie = namedtuple('Serie', ('title', 'values', 'index'))
Label = namedtuple('Label', ('label', 'pos'))
class Margin(object):
def __init__(self, top, right, bottom, left):
self.top = top
self.right = right
self.bottom = bottom
self.left = left
@property
def x(self):
return self.left + self.right
@property
def y(self):
return self.top + self.bottom
from pygal.bar import Bar
from pygal.line import Line
from pygal.xy import XY
from pygal.pie import Pie
from pygal.config import Config

3
pygal/bar.py

@ -10,7 +10,8 @@ class Bar(BaseGraph):
x_step = len(self.series[0].values)
x_pos = [x / float(x_step) for x in range(x_step + 1)
] if x_step > 1 else [0, 1] # Center if only one value
y_pos = self._y_pos(ymin, ymax) if not self.y_labels else map(
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:])

44
pygal/base.py

@ -1,4 +1,5 @@
from pygal import Serie, Margin
from pygal.serie import Serie
from pygal.view import Margin
from pygal.util import round_to_scale
from pygal.svg import Svg
from pygal.config import Config
@ -19,29 +20,32 @@ class BaseGraph(object):
return object.__getattribute__(self.config, attr)
return object.__getattribute__(self, attr)
def _y_pos(self, ymin, ymax):
order = round(math.log10(max(abs(ymin), abs(ymax)))) - 1
if (ymax - ymin) / float(10 ** order) < 4:
def _pos(self, min_, max_, scale):
order = round(math.log10(max(abs(min_), abs(max_)))) - 1
while (max_ - min_) / float(10 ** order) < 4:
order -= 1
step = 10 ** order
step = float(10 ** order)
while (max_ - min_) / step > 20:
step *= 2.
positions = set()
if self.x_start_at_zero:
position = 0
else:
position = round_to_scale(ymin, step)
while position < (ymax + step):
rounded = round_to_scale(position, self.scale)
if ymin <= rounded <= ymax:
position = round_to_scale(min_, step)
while position < (max_ + step):
rounded = round_to_scale(position, scale)
if min_ <= rounded <= max_:
positions.add(rounded)
position += step
if not positions:
return [ymin]
return [min_]
return positions
def _compute_margin(self, x_labels, y_labels):
self.margin.left += 10 + max(
map(len, [l for l, _ in y_labels])
) * 0.6 * self.label_font_size
def _compute_margin(self, x_labels=None, y_labels=None):
if y_labels:
self.margin.left += 10 + max(
map(len, [l for l, _ in y_labels])
) * 0.6 * self.label_font_size
if x_labels:
self.margin.bottom += 10 + self.label_font_size
self.margin.right += 20 + max(
@ -55,10 +59,14 @@ class BaseGraph(object):
def render(self):
if len(self.series) == 0 or sum(
map(len, map(lambda s: s.values, self.series))) == 0:
return
self.validate()
self._draw()
return self.svg.render()
return "No data"
try:
self.validate()
self._draw()
return self.svg.render()
except Exception:
from traceback import format_exc
return format_exc()
def validate(self):
if self.x_labels:

22
pygal/config.py

@ -2,17 +2,31 @@ from pygal.style import DefaultStyle
class Config(object):
width = 800
height = 600
scale = 1
max_scale_step = 10
"""Class holding config values"""
# Graph width and height
width, height = 800, 600
# Scale order range
x_scale = 1
y_scale = 1
# If set to a filename, this will replace the default css
base_css = None
# Style holding values injected in css
style = DefaultStyle
label_font_size = 12
# X labels, must have same len than data.
# Leave it to None to disable x labels display.
x_labels = None
# You can specify explicit y labels (must be list(int))
y_labels = None
# Graph title
# Leave it to None to disable title.
title = None
# Set this to the desired radius in px
rounded_bars = False
# Always include x axis
x_start_at_zero = False
def __init__(self, **kwargs):
"""Can be instanciated with config kwargs"""
self.__dict__.update(kwargs)

39
pygal/css/graph.css

@ -1,3 +1,8 @@
svg {
background-color: {{ style.background }};
box-shadow: 0 0 5px {{ style.foreground }};
}
svg * {
-webkit-transition: 250ms;
-moz-transition: 250ms;
@ -60,31 +65,41 @@ svg * {
opacity: 0;
}
.axis .guides:hover .guide.line {
.axis.y .guides:hover .guide.line, .Line .axis.x .guides:hover .guide.line {
stroke: {{ style.foreground_light }};
opacity: 1;
}
.axis .guides:hover text {
opacity: 1;
fill: {{ style.foreground_light }};
opacity: 1;
}
.series .dots .dot circle {
stroke-width: 9px;
stroke: transparent;
stroke-width: 1px;
}
.series .dots .dot:hover circle {
stroke-width: 5px;
}
.series .dots .dot text {
.series .dots .dot text, .series .bars .bar text, .series .slices .slice text {
opacity: 0;
font-size: 10px;
font-size: 12px;
text-anchor: middle;
alignment-baseline: baseline;
stroke: none;
stroke: {{ style.foreground_light }};
fill: {{ style.foreground_light }};
alignment-baseline: baseline;
text-shadow: 0 0 5px {{ style.background }};
z-index: 9999;
}
.series .dots .dot:hover text {
.series .bars .bar text, .series .slices .slice text {
alignment-baseline: middle;
}
.series .dots .dot:hover text, .series .bars .bar:hover text, .series .slices .slice:hover text {
opacity: 1;
}
@ -98,12 +113,12 @@ svg * {
stroke-width: 2px;
}
.series .rect {
opacity: .8;
.series .rect, .series .slice {
fill-opacity: .8;
}
.series .rect:hover {
opacity: 1;
.series .rect:hover, .series .slice:hover {
fill-opacity: 1;
}

3
pygal/line.py

@ -10,7 +10,8 @@ class Line(BaseGraph):
x_step = len(self.series[0].values)
x_pos = [x / float(x_step - 1) for x in range(x_step)
] if x_step != 1 else [.5] # Center if only one value
y_pos = self._y_pos(ymin, ymax) if not self.y_labels else map(
y_pos = self._pos(
ymin, ymax, self.y_scale) if not self.y_labels else map(
int, self.y_labels)
x_labels = self.x_labels and zip(self.x_labels, x_pos)

27
pygal/pie.py

@ -0,0 +1,27 @@
from pygal.serie import Serie
from pygal.base import BaseGraph
from math import pi
class Pie(BaseGraph):
"""Pie graph"""
def add(self, title, value):
self.series.append(Serie(title, [value], len(self.series)))
def _draw(self):
self._compute_margin()
self.svg.set_view()
self.svg.make_graph()
self.svg.legend([serie.title for serie in self.series])
self.svg.title()
total = float(sum(serie.values[0] for serie in self.series))
current_angle = 0
for serie in self.series:
val = serie.values[0]
angle = 2 * pi * val / total
self.svg.slice(
self.svg.serie(serie.index),
current_angle,
angle, val / total)
current_angle += angle

11
pygal/serie.py

@ -0,0 +1,11 @@
class Serie(object):
def __init__(self, title, values, index):
self.title = title
self.values = values
self.index = index
class Label(object):
def __init__(self, label, pos):
self.label = label
self.pos = pos

50
pygal/svg.py

@ -2,6 +2,7 @@ import os
from lxml import etree
from pygal.view import View
from pygal.style import DefaultStyle
from math import cos, sin, pi
class Svg(object):
@ -52,14 +53,15 @@ class Svg(object):
return etree.SubElement(parent, tag, attrib)
def set_view(self, ymin, ymax, xmin=0, xmax=1):
def set_view(self, ymin=0, ymax=1, xmin=0, xmax=1):
self.view = View(
self.graph.width - self.graph.margin.x,
self.graph.height - self.graph.margin.y,
xmin, xmax, ymin, ymax)
def make_graph(self):
self.graph_node = self.node(class_='graph')
self.graph_node = self.node(
class_='graph %s' % self.graph.__class__.__name__)
self.node(self.graph_node, 'rect',
class_='background',
x=0, y=0,
@ -131,19 +133,19 @@ class Svg(object):
return self.node(
self.plot, class_='series serie-%d color-%d' % (serie, serie))
def line(self, serie, values, origin=None):
def line(self, serie_node, values, origin=None):
view_values = map(self.view, values)
if origin == None:
origin = '%f %f' % view_values[0]
dots = self.node(serie, class_="dots")
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][1])
svg_values = ' '.join(map(lambda x: '%f %f' % x, view_values))
self.node(serie, 'path',
self.node(serie_node, 'path',
d='M%s L%s' % (origin, svg_values), class_='line')
def bar(self, serie_node, serie, values, origin=None):
@ -154,6 +156,7 @@ class Svg(object):
"""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
@ -165,15 +168,48 @@ class Svg(object):
bar_inner_width = bar_width - 2 * bar_padding
offset = serie.index * bar_width + bar_padding
height = self.view.y(0) - y
x = x + padding + offset
y_txt = y + height / 2
if height < 0:
y = y + height
height = -height
self.node(serie_node, 'rect',
x=x + padding + offset,
y_txt = y + height / 2
bar = self.node(bars, class_='bar')
self.node(bar, 'rect',
x=x,
y=y,
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,
).text = str(values[i][1][1])
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
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),
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,
y=center[1] - text_r * sin(text_angle),
).text = '{:.2%}'.format(perc)
def render(self):
return etree.tostring(

14
pygal/view.py

@ -1,3 +1,17 @@
class Margin(object):
def __init__(self, top, right, bottom, left):
self.top = top
self.right = right
self.bottom = bottom
self.left = left
@property
def x(self):
return self.left + self.right
@property
def y(self):
return self.top + self.bottom
class Box(object):

31
pygal/xy.py

@ -0,0 +1,31 @@
from pygal.base import BaseGraph
class XY(BaseGraph):
"""XY Line graph"""
def _draw(self):
for serie in self.series:
serie.values = sorted(serie.values, key=lambda x: x[0])
xvals = [val[0] for serie in self.series for val in serie.values]
yvals = [val[1] for serie in self.series for val in serie.values]
xmin, xmax = min(xvals), max(xvals)
ymin, ymax = min(yvals), max(yvals)
x_pos = self._pos(xmin, xmax, self.x_scale)
y_pos = self._pos(ymin, ymax, self.y_scale)
x_labels = zip(map(str, x_pos), x_pos)
y_labels = zip(map(str, y_pos), y_pos)
self._compute_margin(x_labels, y_labels)
self.svg.set_view(ymin, ymax, xmin, xmax)
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()
for serie in self.series:
self.svg.line(
self.svg.serie(serie.index), serie.values)
Loading…
Cancel
Save