Browse Source

Big refactor

pull/8/head
Florian Mounier 13 years ago
parent
commit
d74de57134
  1. 2
      pygal/__init__.py
  2. 79
      pygal/bar.py
  3. 56
      pygal/base.py
  4. 14
      pygal/config.py
  5. 81
      pygal/line.py
  6. 2
      pygal/style.py
  7. 67
      pygal/svg.py
  8. 11
      pygal/test/test_bar.py
  9. 8
      pygal/test/test_line.py
  10. 7
      pygal/test/test_svg.py
  11. 30
      pygal/test/test_util.py
  12. 15
      pygal/util.py
  13. 4
      pygal/view.py

2
pygal/__init__.py

@ -1,6 +1,6 @@
from collections import namedtuple
Serie = namedtuple('Serie', ('title', 'values'))
Serie = namedtuple('Serie', ('title', 'values', 'index'))
Label = namedtuple('Label', ('label', 'pos'))

79
pygal/bar.py

@ -1,79 +1,32 @@
from pygal import Serie, Margin, Label
from pygal.svg import Svg
from pygal.util import round_to_int, round_to_float
from pygal import Margin, Label
from pygal.base import BaseGraph
class Bar(BaseGraph):
"""Bar graph"""
def __init__(self, width, height, scale=1, style=None):
self.width = width
self.height = height
self.svg = Svg(width, height, style=style)
self.label_font_size = 12
self.series = []
self.scale = scale
self.x_labels = self.y_labels = self.title = None
rnd = round_to_float if self.scale < 1 else round_to_int
self.round = lambda x: rnd(x, self.scale)
def add(self, title, values):
self.series.append(Serie(title, values))
def _label(self, number):
return Label(*self.round(number))
def _y_labels(self, ymin, ymax):
step = (ymax - ymin) / 20.
if not step:
return [self._label(ymin)]
label = ymin
labels = set()
while label < (ymax + step):
labels.add(self._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:
assert len(self.series[0].values) == len(serie.values)
def draw(self):
self.validate()
def _draw(self):
ymin, ymax = 0, max([
val for serie in self.series for val in serie.values])
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(
int, self.y_labels)
x_ranges = zip(x_pos, x_pos[1:])
vals = [val for serie in self.series for val in serie.values]
margin = Margin(*(4 * [10]))
ymin, ymax = 0, max(vals)
if self.x_labels:
x_labels = [Label(label, sum(x_ranges[i]) / 2)
for i, label in enumerate(self.x_labels)]
y_labels = self.y_labels or self._y_labels(ymin, ymax)
series_labels = [serie.title for serie in self.series]
margin.left += 10 + max(
map(len, [l.label for l in y_labels])) * 0.6 * self.label_font_size
if self.x_labels:
margin.bottom += 10 + self.label_font_size
margin.right += 20 + max(
map(len, series_labels)) * 0.6 * self.label_font_size
margin.top += 10 + self.label_font_size
x_labels = self.x_labels and zip(self.x_labels, [
sum(x_range) / 2 for x_range in x_ranges])
y_labels = zip(map(str, y_pos), y_pos)
# Actual drawing
self.svg.set_view(margin, ymin, ymax)
self.svg.graph(margin)
if self.x_labels:
self.svg.x_axis(x_labels)
self._compute_margin(x_labels, y_labels)
self.svg.set_view(ymin, ymax)
self.svg.make_graph()
self.svg.x_axis(x_labels)
self.svg.y_axis(y_labels)
self.svg.legend(margin, series_labels)
self.svg.title(margin, self.title)
self.svg.legend([serie.title for serie in self.series])
self.svg.title()
for serie_index, serie in enumerate(self.series):
serie_node = self.svg.serie(serie_index)
self.svg.bar(serie_node, [

56
pygal/base.py

@ -1,13 +1,63 @@
from pygal import Serie, Label, Margin
from pygal.util import round_to_scale
from pygal.svg import Svg
from pygal.config import Config
class BaseGraph(object):
"""Graphs commons"""
def __init__(self, config=None):
self.config = config or Config()
self.svg = Svg(self)
self.series = []
self.margin = Margin(*([10] * 4))
def __getattr__(self, attr):
if attr in dir(self.config):
return object.__getattribute__(self.config, attr)
return object.__getattribute__(self, attr)
def _y_pos(self, ymin, ymax):
step = (ymax - ymin) / float(self.max_scale_step)
position = ymin
if not step:
return [position]
positions = set()
while position < (ymax + step):
positions.add(round_to_scale(position, self.scale))
position += step
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
if x_labels:
self.margin.bottom += 10 + self.label_font_size
self.margin.right += 20 + max(
map(len, [serie.title for serie in self.series])
) * 0.6 * self.label_font_size
self.margin.top += 10 + self.label_font_size
def add(self, title, values):
self.series.append(Serie(title, values, len(self.series)))
def render(self):
self.draw()
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()
def _in_browser(self, *args, **kwargs):
def validate(self):
if self.x_labels:
assert len(self.series[0].values) == len(self.x_labels)
for serie in self.series:
assert len(self.series[0].values) == len(serie.values)
def _in_browser(self):
from lxml.html import open_in_browser
self.draw(*args, **kwargs)
self._draw()
open_in_browser(self.svg.root, encoding='utf-8')

14
pygal/config.py

@ -0,0 +1,14 @@
from pygal.style import DefaultStyle
class Config(object):
width = 800
height = 600
scale = 1
max_scale_step = 20
base_css = None
style = DefaultStyle
label_font_size = 12
x_labels = None
y_labels = None
title = None

81
pygal/line.py

@ -1,80 +1,33 @@
from pygal import Serie, Margin, Label
from pygal.svg import Svg
from pygal.util import round_to_int, round_to_float
from pygal.util import round_to_scale
from pygal.base import BaseGraph
class Line(BaseGraph):
"""Line graph"""
def __init__(self, width, height, scale=1, style=None):
self.width = width
self.height = height
self.svg = Svg(width, height, style=style)
self.label_font_size = 12
self.series = []
self.scale = scale
self.x_labels = self.y_labels = self.title = None
rnd = round_to_float if self.scale < 1 else round_to_int
self.round = lambda x: rnd(x, self.scale)
def add(self, title, values):
self.series.append(Serie(title, values))
def _label(self, number):
return Label(*self.round(number))
def _y_labels(self, ymin, ymax):
step = (ymax - ymin) / 20.
if not step:
return [self._label(ymin)]
label = ymin
labels = set()
while label < (ymax + step):
labels.add(self._label(label))
label += step
return labels
def validate(self):
if self.x_labels:
assert len(self.series[0].values) == len(self.x_labels)
for serie in self.series:
assert len(self.series[0].values) == len(serie.values)
def draw(self):
def _draw(self):
vals = [val for serie in self.series for val in serie.values]
if not vals:
return
self.validate()
ymin, ymax = min(vals), max(vals)
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
margin = Margin(*(4 * [10]))
ymin, ymax = min(vals), max(vals)
if self.x_labels:
x_labels = [Label(label, x_pos[i])
for i, label in enumerate(self.x_labels)]
y_labels = self.y_labels or self._y_labels(ymin, ymax)
series_labels = [serie.title for serie in self.series]
margin.left += 10 + max(
map(len, [l.label for l in y_labels])) * 0.6 * self.label_font_size
if self.x_labels:
margin.bottom += 10 + self.label_font_size
margin.right += 20 + max(
map(len, series_labels)) * 0.6 * self.label_font_size
margin.top += 10 + self.label_font_size
y_pos = self._y_pos(ymin, ymax) if not self.y_labels else map(
int, self.y_labels)
# Actual drawing
self.svg.set_view(margin, ymin, ymax)
self.svg.graph(margin)
if self.x_labels:
self.svg.x_axis(x_labels)
x_labels = self.x_labels and zip(self.x_labels, x_pos)
y_labels = zip(map(str, y_pos), y_pos)
self._compute_margin(x_labels, y_labels)
self.svg.set_view(ymin, ymax)
self.svg.make_graph()
self.svg.x_axis(x_labels)
self.svg.y_axis(y_labels)
self.svg.legend(margin, series_labels)
self.svg.title(margin, self.title)
self.svg.legend([serie.title for serie in self.series])
self.svg.title()
for serie_index, serie in enumerate(self.series):
serie_node = self.svg.serie(serie_index)
self.svg.line(serie_node, [
self.svg.line(
self.svg.serie(serie_index), [
(x_pos[i], v)
for i, v in enumerate(serie.values)])

2
pygal/style.py

@ -37,3 +37,5 @@ LightStyle = Style(
colors=('#242424', '#9f6767', '#92ac68',
'#d0d293', '#9aacc3', '#bb77a4',
'#77bbb5', '#777777'))
styles = [DefaultStyle, LightStyle]

67
pygal/svg.py

@ -8,15 +8,12 @@ class Svg(object):
"""Svg object"""
ns = 'http://www.w3.org/2000/svg'
def __init__(self, width, height, base_css=None, style=None):
self.width = width
self.height = height
self.margin = ()
self.style = style or DefaultStyle
def __init__(self, graph):
self.graph = graph
self.root = etree.Element(
"{%s}svg" % self.ns,
attrib={
'viewBox': '0 0 %d %d' % (width, height)
'viewBox': '0 0 %d %d' % (self.graph.width, self.graph.height)
},
nsmap={
None: self.ns,
@ -24,10 +21,8 @@ class Svg(object):
})
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)
self.add_style(self.graph.base_css or os.path.join(
os.path.dirname(__file__), 'css', 'graph.css'))
def add_style(self, css):
style = self.node(self.defs, 'style', type='text/css')
@ -41,7 +36,7 @@ class Svg(object):
.replace(' }}', '\x00')
.replace('}', '}}')
.replace('\x00', '}')
.format(style=self.style))
.format(style=self.graph.style))
def node(self, parent=None, tag='g', attrib=None, **extras):
if parent is None:
@ -57,22 +52,23 @@ class Svg(object):
return etree.SubElement(parent, tag, attrib)
def set_view(self, margin, ymin, ymax, xmin=0, xmax=1):
def set_view(self, ymin, ymax, xmin=0, xmax=1):
self.view = View(
self.width - margin.x,
self.height - margin.y,
self.graph.width - self.graph.margin.x,
self.graph.height - self.graph.margin.y,
xmin, xmax, ymin, ymax)
def graph(self, margin):
self.graph = self.node(class_='graph')
self.node(self.graph, 'rect',
def make_graph(self):
self.graph_node = self.node(class_='graph')
self.node(self.graph_node, 'rect',
class_='background',
x=0, y=0,
width=self.width,
height=self.height)
width=self.graph.width,
height=self.graph.height)
self.plot = self.node(
self.graph, class_="plot",
transform="translate(%d, %d)" % (margin.left, margin.top))
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,
@ -85,16 +81,18 @@ class Svg(object):
self.node(axis, 'path',
d='M%f %f v%f' % (0, 0, self.view.height),
class_='line')
for label in labels:
if not labels:
return
for label, position in labels:
guides = self.node(axis, class_='guides')
x = self.view.x(label.pos)
x = self.view.x(position)
if x != 0:
self.node(guides, 'path',
d='M%f %f v%f' % (x, 0, self.view.height),
class_='guide line')
text = self.node(guides, 'text', x=x, y=self.view.height + 5)
text.text = label.label
text.text = label
def y_axis(self, labels):
axis = self.node(self.plot, class_="axis y")
@ -102,31 +100,32 @@ class Svg(object):
self.node(axis, 'path',
d='M%f %f h%f' % (0, self.view.height, self.view.width),
class_='line')
for label in labels:
for label, position in labels:
guides = self.node(axis, class_='guides')
y = self.view.y(label.pos)
y = self.view.y(position)
if y != self.view.height:
self.node(guides, 'path',
d='M%f %f h%f' % (0, y, self.view.width),
class_='guide line')
text = self.node(guides, 'text', x=-5, y=y)
text.text = label.label
text.text = label
def legend(self, margin, titles):
def legend(self, titles):
legend = self.node(
self.graph, class_='legend',
self.graph_node, class_='legend',
transform='translate(%d, %d)' % (
margin.left + self.view.width + 10, margin.top + 10))
self.graph.margin.left + self.view.width + 10,
self.graph.margin.top + 10))
for i, title in enumerate(titles):
self.node(legend, 'rect', x=0, y=i * 15,
width=8, height=8, class_="color-%d" % i,
).text = title
self.node(legend, 'text', x=15, y=i * 15).text = title
def title(self, margin, title):
self.node(self.graph, 'text', class_='title',
x=margin.left + self.view.width / 2,
y=10).text = title
def title(self):
self.node(self.graph_node, 'text', class_='title',
x=self.graph.margin.left + self.view.width / 2,
y=10).text = self.graph.title
def serie(self, serie):
return self.node(

11
pygal/test/test_bar.py

@ -3,18 +3,9 @@ from math import cos, sin
def test_simple_bar():
bar = Bar(800, 600)
bar = Bar()
rng = [12, 3, 30, 4, 40, 10, 9, 2]
bar.add('test1', rng)
bar.x_labels = map(str, rng)
bar.title = "Bar test"
bar.render()
def test_null_bar():
bar = Bar(800, 600, scale=.25)
rng = [1, 1]
bar.add('test1', rng)
bar.x_labels = map(str, rng)
bar.title = "Bar test"
bar._in_browser()

8
pygal/test/test_line.py

@ -3,7 +3,7 @@ from math import cos, sin
def test_simple_line():
line = Line(800, 600)
line = Line()
rng = range(-30, 31, 5)
line.add('test1', [cos(x / 10.) for x in rng])
line.add('test2', [sin(x / 10.) for x in rng])
@ -14,18 +14,18 @@ def test_simple_line():
def test_one_dot():
line = Line(800, 600)
line = Line()
line.add('one dot', [12])
line.x_labels = ['one']
line.render()
def test_no_dot():
line = Line(800, 600)
line = Line()
line.add('no dot', [])
line.render()
def test_no_dot_at_all():
line = Line(800, 600)
line = Line()
line.render()

7
pygal/test/test_svg.py

@ -1,8 +1,13 @@
from pygal.svg import Svg
from pygal.config import Config
def test_root():
svg = Svg(800, 600)
class RootConfig(Config):
width = 800
height = 600
svg = Svg(RootConfig)
assert svg.render().startswith('\n'.join((
'<?xml version=\'1.0\' encoding=\'utf-8\'?>',
'<svg xmlns:xlink="http://www.w3.org/1999/xlink" '

30
pygal/test/test_util.py

@ -2,21 +2,21 @@ from pygal.util import round_to_int, round_to_float
def test_round_to_int():
assert round_to_int(154231, 1000) == ('154000', 154000)
assert round_to_int(154231, 10) == ('154230', 154230)
assert round_to_int(154231, 100000) == ('200000', 200000)
assert round_to_int(154231, 50000) == ('150000', 150000)
assert round_to_int(154231, 500) == ('154000', 154000)
assert round_to_int(154231, 200) == ('154200', 154200)
assert round_to_int(154361, 200) == ('154400', 154400)
assert round_to_int(154231, 1000) == 154000
assert round_to_int(154231, 10) == 154230
assert round_to_int(154231, 100000) == 200000
assert round_to_int(154231, 50000) == 150000
assert round_to_int(154231, 500) == 154000
assert round_to_int(154231, 200) == 154200
assert round_to_int(154361, 200) == 154400
def test_round_to_float():
assert round_to_float(12.01934, .01) == ('12.02', 12.02)
assert round_to_float(12.01134, .01) == ('12.01', 12.01)
assert round_to_float(12.1934, .1) == ('12.2', 12.2)
assert round_to_float(12.1134, .1) == ('12.1', 12.1)
assert round_to_float(12.1134, .001) == ('12.113', 12.113)
assert round_to_float(12.1134, .00001) == ('12.11340', 12.1134)
assert round_to_float(12.1934, .5) == ('12.0', 12.0)
assert round_to_float(12.2934, .5) == ('12.5', 12.5)
assert round_to_float(12.01934, .01) == 12.02
assert round_to_float(12.01134, .01) == 12.01
assert round_to_float(12.1934, .1) == 12.2
assert round_to_float(12.1134, .1) == 12.1
assert round_to_float(12.1134, .001) == 12.113
assert round_to_float(12.1134, .00001) == 12.1134
assert round_to_float(12.1934, .5) == 12.0
assert round_to_float(12.2934, .5) == 12.5

15
pygal/util.py

@ -4,15 +4,16 @@ from math import floor
def round_to_int(number, precision):
rounded = (int(number) + precision / 2) / precision * precision
return str(int(rounded)), rounded
return rounded
# def round_to_float(number, precision):
# decimal = Decimal(str(number))
# rounded = decimal.quantize(Decimal(str(precision)))
# return str(rounded), float(rounded)
def round_to_float(number, precision):
rounded = Decimal(
floor((number + precision / 2) / precision)) * Decimal(str(precision))
return str(rounded), float(rounded)
return float(rounded)
def round_to_scale(number, precision):
if precision < 1:
return round_to_float(number, precision)
return round_to_int(number, precision)

4
pygal/view.py

@ -14,8 +14,8 @@ class View(object):
self.height = height
xrng = (xmax - xmin) or 1
yrng = (ymax - ymin) or 1
# if yrng == 1:
# ymin -= .5x
if (ymax - ymin) == 0:
ymin -= .5
self.box = Box(xmin, ymin, xrng, yrng)
def x(self, x):

Loading…
Cancel
Save