Browse Source

Here comes the lines

pull/8/head
Florian Mounier 13 years ago
parent
commit
5913e43e69
  1. 17
      pygal/__init__.py
  2. 64
      pygal/css/graph.css
  3. 58
      pygal/line.py
  4. 97
      pygal/svg.py
  5. 5
      pygal/test/test_line.py
  6. 4
      pygal/test/test_svg.py
  7. 28
      pygal/view.py
  8. 4
      relauncher

17
pygal/__init__.py

@ -1,3 +1,20 @@
from collections import namedtuple
Serie = namedtuple('Serie', ('title', 'values'))
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

64
pygal/css/graph.css

@ -0,0 +1,64 @@
.graph > .background {
fill: black;
}
.plot > .background {
fill: #111;
}
.axis text {
fill: #777;
font-size: 12px;
font-family: sans;
}
.axis.x text {
text-anchor: middle;
alignment-baseline: baseline;
}
.axis.y text {
text-anchor: end;
alignment-baseline: middle;
}
.axis .line {
stroke: #ccc;
}
.axis .guide.line {
stroke: #555;
stroke-dasharray: 5,5;
}
.series .dots .dot text {
font-size: 10px;
text-anchor: middle;
alignment-baseline: baseline;
stroke: none;
fill: none;
}
.series .dots .dot:hover text {
fill: #ccc;
}
.series .line {
fill: none;
stroke-width: 1.5px;
}
.series .line:hover {
fill: none;
stroke-width: 3px;
}
.serie-0 {
stroke: blue;
fill: blue;
}
.serie-1 {
stroke: red;
fill: red;
}

58
pygal/line.py

@ -1,4 +1,4 @@
from pygal import Serie
from pygal import Serie, Margin, Label
from pygal.svg import Svg
from pygal.base import BaseGraph
@ -10,23 +10,55 @@ class Line(BaseGraph):
self.width = width
self.height = height
self.svg = Svg(width, height)
self.label_font_size = 12
self.series = []
self.x_labels = None
def add(self, title, values):
self.series.append(
Serie(title, values))
def draw(self):
min_value = min((val
for serie in self.series
for val in serie.values))
max_value = max((val
for serie in self.series
for val in serie.values))
self.svg.graph(min_value, max_value)
def set_labels(self, labels):
values = float(len(labels) - 1)
self.x_labels = [Label(label, i / values)
for i, label in enumerate(labels)]
def y_labels(self, ymin, ymax):
step = (ymax - ymin) / 20.
label = ymin
labels = []
while label < ymax:
labels.append(Label(str(label), label))
label += step
return labels
def validate(self):
assert len(self.series)
if self.x_labels:
assert len(self.series[0].values) == len(self.x_labels)
for serie in self.series:
n_values = len(serie.values) - 1
x_spacing = self.width / n_values
self.svg.line([
(i * x_spacing, v)
assert len(self.series[0].values) == len(serie.values)
def draw(self):
self.validate()
vals = [val for serie in self.series for val in serie.values]
margin = Margin(*(4 * [20]))
ymin, ymax = min(vals), max(vals)
x_labels = self.x_labels
y_labels = self.y_labels(ymin, ymax)
margin.left += 10 + max(
map(len, [l.label for l in y_labels])) * 0.6 * self.label_font_size
margin.bottom += 10 + self.label_font_size
# Actual drawing
self.svg.set_view(margin, ymin, ymax)
self.svg.graph(margin)
self.svg.x_axis(x_labels)
self.svg.y_axis(y_labels)
for serie_index, serie in enumerate(self.series):
serie_node = self.svg.serie(serie_index)
self.svg.line(serie_node, [
(x_labels[i].pos, v)
for i, v in enumerate(serie.values)])

97
pygal/svg.py

@ -1,13 +1,16 @@
import os
from lxml import etree
from pygal.view import View
class Svg(object):
"""Svg object"""
ns = 'http://www.w3.org/2000/svg'
def __init__(self, width, height):
def __init__(self, width, height, base_css=None):
self.width = width
self.height = height
self.margin = ()
self.root = etree.Element(
"{%s}svg" % self.ns,
attrib={
@ -18,6 +21,17 @@ class Svg(object):
'xlink': 'http://www.w3.org/1999/xlink',
})
self.defs = self.node(tag='defs')
base_css = base_css or os.path.join(
os.path.dirname(__file__), 'css', 'graph.css')
self.add_style(base_css)
def add_style(self, css):
style = self.node(self.defs, 'style', type='text/css')
with open(css) as f:
style.text = f.read()
def node(self, parent=None, tag='g', attrib=None, **extras):
if parent is None:
parent = self.root
@ -32,22 +46,71 @@ class Svg(object):
return etree.SubElement(parent, tag, attrib)
def format_coords(self, xy):
return '%f %f' % xy
def graph(self, min_value, max_value):
self.graph = self.node(
id='graph',
transform="scale(1, %d) translate(0, %d)" % (
self.height / (max_value - min_value), -min_value))
self.node(self.graph, 'rect', id='graph_background',
x=0, y=0, width=self.width, height=self.height)
def line(self, values, origin=None):
origin = self.format_coords(origin or values[0])
values = ' '.join(map(self.format_coords, values))
self.node(self.graph, 'path', style="stroke: blue",
d='M%s L%s' % (origin, values))
def set_view(self, margin, ymin, ymax, xmin=0, xmax=1):
self.view = View(
self.width - margin.x,
self.height - margin.y,
xmin, xmax, ymin, ymax)
def graph(self, margin):
self.graph = self.node(class_='graph')
self.node(self.graph, 'rect',
class_='background',
x=0, y=0,
width=self.width,
height=self.height)
self.plot = self.node(
self.graph, class_="plot",
transform="translate(%d, %d)" % (margin.left, 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, labels):
axis = self.node(self.plot, class_="axis x")
# Plot axis
self.node(axis, 'path',
d='M%f %f v%f' % (0, 0, self.view.height),
class_='line')
for label in labels:
x = self.view.x(label.pos)
text = self.node(axis, 'text', x=x, y=self.view.height + 5)
text.text = label.label
def y_axis(self, labels):
axis = self.node(self.plot, class_="axis y")
# Plot axis
self.node(axis, 'path',
d='M%f %f h%f' % (0, self.view.height, self.view.width),
class_='line')
for label in labels:
y = self.view.y(label.pos)
if y != self.view.height:
self.node(axis, 'path',
d='M%f %f h%f' % (0, y, self.view.width),
class_='guide line')
text = self.node(axis, 'text', x=-5, y=y)
text.text = label.label
def serie(self, serie):
return self.node(self.plot, class_='series serie-%d' % serie)
def line(self, serie, values, origin=None):
view_values = map(self.view, values)
if origin == None:
origin = '%f %f' % view_values[0]
dots = self.node(serie, 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',
d='M%s L%s' % (origin, svg_values), class_='line')
def render(self):
return etree.tostring(

5
pygal/test/test_line.py

@ -1,7 +1,10 @@
from pygal.line import Line
from math import cos, sin
def test_simple_line():
line = Line(800, 600)
line.add('test', [10, 20, 5, 17])
line.add('test2', [cos(x / 10.) for x in range(-30, 30, 5)])
line.add('test2', [sin(x / 10.) for x in range(-30, 30, 5)])
line.set_labels(map(str, range(-30, 30, 5)))
line._in_browser()

4
pygal/test/test_svg.py

@ -3,9 +3,9 @@ from pygal.svg import Svg
def test_root():
svg = Svg(800, 600)
assert svg.render() == ('\n'.join((
assert svg.render().startswith('\n'.join((
'<?xml version=\'1.0\' encoding=\'utf-8\'?>',
'<svg xmlns:xlink="http://www.w3.org/1999/xlink" '
'xmlns="http://www.w3.org/2000/svg" '
'viewBox="0 0 800 600"/>',
'viewBox="0 0 800 600">',
'')))

28
pygal/view.py

@ -0,0 +1,28 @@
class Box(object):
def __init__(self, x, y, width, height):
self.x = x
self.y = y
self.width = width
self.height = height
class View(object):
def __init__(self, width, height, xmin, xmax, ymin, ymax):
self.width = width
self.height = height
self.box = Box(xmin, ymin, xmax - xmin, ymax - ymin)
def x(self, x):
return self.width * (x - self.box.x) / float(self.box.width)
def y(self, y):
return (self.height - self.height *
(y - self.box.y) / float(self.box.height))
def __call__(self, xy):
x, y = xy
return (
self.x(x),
self.y(y))

4
relauncher

@ -0,0 +1,4 @@
#!/bin/sh
while inotifywait -e modify **/*.py >> ~/.log/inotifywait.log 2>&1; do
py.test pygal/test
done
Loading…
Cancel
Save