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 from collections import namedtuple
Serie = namedtuple('Serie', ('title', 'values', 'index')) from pygal.bar import Bar
Label = namedtuple('Label', ('label', 'pos')) from pygal.line import Line
from pygal.xy import XY
from pygal.pie import Pie
class Margin(object): from pygal.config import Config
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

3
pygal/bar.py

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

22
pygal/config.py

@ -2,17 +2,31 @@ from pygal.style import DefaultStyle
class Config(object): class Config(object):
width = 800 """Class holding config values"""
height = 600
scale = 1 # Graph width and height
max_scale_step = 10 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 base_css = None
# Style holding values injected in css
style = DefaultStyle style = DefaultStyle
label_font_size = 12 label_font_size = 12
# X labels, must have same len than data.
# Leave it to None to disable x labels display.
x_labels = None x_labels = None
# You can specify explicit y labels (must be list(int))
y_labels = None y_labels = None
# Graph title
# Leave it to None to disable title.
title = None title = None
# Set this to the desired radius in px
rounded_bars = False
# Always include x axis
x_start_at_zero = False x_start_at_zero = False
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Can be instanciated with config kwargs"""
self.__dict__.update(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 * { svg * {
-webkit-transition: 250ms; -webkit-transition: 250ms;
-moz-transition: 250ms; -moz-transition: 250ms;
@ -60,31 +65,41 @@ svg * {
opacity: 0; opacity: 0;
} }
.axis .guides:hover .guide.line { .axis.y .guides:hover .guide.line, .Line .axis.x .guides:hover .guide.line {
stroke: {{ style.foreground_light }}; stroke: {{ style.foreground_light }};
opacity: 1; opacity: 1;
} }
.axis .guides:hover text { .axis .guides:hover text {
opacity: 1;
fill: {{ style.foreground_light }}; fill: {{ style.foreground_light }};
opacity: 1;
} }
.series .dots .dot circle { .series .dots .dot circle {
stroke-width: 9px; stroke-width: 1px;
stroke: transparent; }
.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; opacity: 0;
font-size: 10px; font-size: 12px;
text-anchor: middle; text-anchor: middle;
alignment-baseline: baseline; alignment-baseline: baseline;
stroke: none; stroke: {{ style.foreground_light }};
fill: {{ 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; opacity: 1;
} }
@ -98,12 +113,12 @@ svg * {
stroke-width: 2px; stroke-width: 2px;
} }
.series .rect { .series .rect, .series .slice {
opacity: .8; fill-opacity: .8;
} }
.series .rect:hover { .series .rect:hover, .series .slice:hover {
opacity: 1; fill-opacity: 1;
} }

3
pygal/line.py

@ -10,7 +10,8 @@ class Line(BaseGraph):
x_step = len(self.series[0].values) x_step = len(self.series[0].values)
x_pos = [x / float(x_step - 1) for x in range(x_step) x_pos = [x / float(x_step - 1) for x in range(x_step)
] if x_step != 1 else [.5] # Center if only one value ] 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) int, self.y_labels)
x_labels = self.x_labels and zip(self.x_labels, x_pos) 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 lxml import etree
from pygal.view import View from pygal.view import View
from pygal.style import DefaultStyle from pygal.style import DefaultStyle
from math import cos, sin, pi
class Svg(object): class Svg(object):
@ -52,14 +53,15 @@ class Svg(object):
return etree.SubElement(parent, tag, attrib) 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.view = View(
self.graph.width - self.graph.margin.x, self.graph.width - self.graph.margin.x,
self.graph.height - self.graph.margin.y, self.graph.height - self.graph.margin.y,
xmin, xmax, ymin, ymax) xmin, xmax, ymin, ymax)
def make_graph(self): 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', self.node(self.graph_node, 'rect',
class_='background', class_='background',
x=0, y=0, x=0, y=0,
@ -131,19 +133,19 @@ class Svg(object):
return self.node( return self.node(
self.plot, class_='series serie-%d color-%d' % (serie, serie)) 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) view_values = map(self.view, values)
if origin == None: if origin == None:
origin = '%f %f' % view_values[0] 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): for i, (x, y) in enumerate(view_values):
dot = self.node(dots, class_='dot') dot = self.node(dots, class_='dot')
self.node(dot, 'circle', cx=x, cy=y, r=2.5) self.node(dot, 'circle', cx=x, cy=y, r=2.5)
self.node(dot, 'text', x=x, y=y).text = str(values[i][1]) self.node(dot, 'text', x=x, y=y).text = str(values[i][1])
svg_values = ' '.join(map(lambda x: '%f %f' % x, view_values)) 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') d='M%s L%s' % (origin, svg_values), class_='line')
def bar(self, serie_node, serie, values, origin=None): def bar(self, serie_node, serie, values, origin=None):
@ -154,6 +156,7 @@ class Svg(object):
"""Project range""" """Project range"""
return (self.view(rng[0]), self.view(rng[1])) return (self.view(rng[0]), self.view(rng[1]))
bars = self.node(serie_node, class_="bars")
view_values = map(view, values) view_values = map(view, values)
for i, ((x, y), (X, Y)) in enumerate(view_values): for i, ((x, y), (X, Y)) in enumerate(view_values):
# x and y are left range coords and X, Y right ones # 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 bar_inner_width = bar_width - 2 * bar_padding
offset = serie.index * bar_width + bar_padding offset = serie.index * bar_width + bar_padding
height = self.view.y(0) - y height = self.view.y(0) - y
x = x + padding + offset
y_txt = y + height / 2
if height < 0: if height < 0:
y = y + height y = y + height
height = -height height = -height
self.node(serie_node, 'rect', y_txt = y + height / 2
x=x + padding + offset, bar = self.node(bars, class_='bar')
self.node(bar, 'rect',
x=x,
y=y, y=y,
rx=self.graph.rounded_bars * 1,
ry=self.graph.rounded_bars * 1,
width=bar_inner_width, width=bar_inner_width,
height=height, height=height,
class_='rect') 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): def render(self):
return etree.tostring( 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): 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