Browse Source

Fix some bugs + add generated tests + support external style sheets

pull/8/head
Florian Mounier 13 years ago
parent
commit
d7277f1f51
  1. 7
      demo/simple_test.py
  2. 10
      pygal/config.py
  3. 48
      pygal/css/base.css
  4. 68
      pygal/css/graph.css
  5. 112
      pygal/css/style.css
  6. 4
      pygal/graph/bar.py
  7. 14
      pygal/graph/base.py
  8. 4
      pygal/graph/graph.py
  9. 2
      pygal/serie.py
  10. 57
      pygal/svg.py
  11. 31
      pygal/test/__init__.py
  12. 3
      pygal/test/test_config.py
  13. 61
      pygal/test/test_graph.py
  14. 4
      pygal/util.py

7
demo/simple_test.py

@ -28,7 +28,7 @@ dot = Dot()
dot.x_labels = map(str, range(4))
dot.add('a', [1, lnk(3, 'Foo'), 5, 3])
dot.add('b', [2, -2, 0, 2])
dot.add('b', [2, 2, 0, 2])
dot.add('c', [5, 1, 5, lnk(3, 'Bar')])
dot.add('d', [5, 5, lnk(0, 'Babar'), 3])
@ -45,7 +45,8 @@ bar.add('1234', [
bar.add('4321', [40, {'value': 30, 'label': 'Thirty', 'xlink': 'http://google.com?q=30'}, 20, 10])
bar.x_labels = map(str, range(1, 5))
bar.logarithmic = True
bar.zero = 1
# bar.included_js = []
# bar.external_js = [
# 'http://localhost:7575/svg.jquery.js',
@ -163,7 +164,7 @@ config.x_labels = (
'black', 'red', 'blue', 'yellow', 'orange', 'green', 'white')
# config.interpolate = 'nearest'
radar = Radar(config)
radar.add('test', [1, 4, lnk(10), 5, None, -2, 5])
radar.add('test', [1, 4, lnk(10), 5, None, 2, 5])
radar.add('test2', [10, 2, 0, 5, 1, 9, 4])
radar.title = "Radar test"

10
pygal/config.py

@ -21,7 +21,6 @@
Config module with all options
"""
import os
from pygal.style import DefaultStyle
@ -40,11 +39,10 @@ class Config(object):
human_readable = False
#: Display values in logarithmic scale
logarithmic = False
#: If set to a filename, this will replace the default css
base_css = None
#: or default js
included_js = []
external_js = [
#: List of css file, can be an absolute file path or an external link
css = ['style.css', 'graph.css'] # Relative path to pygal css
#: List of js file, can be a filepath or an external link
js = [
'https://raw.github.com/Kozea/pygal.js/master/svg.jquery.js',
'https://raw.github.com/Kozea/pygal.js/master/pygal-tooltips.js'
]

48
pygal/css/base.css

@ -0,0 +1,48 @@
/*
* This file is part of pygal
*
* A python svg graph plotting library
* Copyright © 2012 Kozea
* This library is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with pygal. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Font-sizes from config, override with care
*/
.title {
font-family: monospace;
font-size: {{ font_sizes.title }};
}
.legends .legend text {
font-family: monospace;
font-size: {{ font_sizes.legend }};
}
.axis text {
font-family: monospace;
font-size: {{ font_sizes.label }};
}
.series text {
font-family: monospace;
font-size: {{ font_sizes.value }};
}
#tooltip text {
font-family: monospace;
font-size: {{ font_sizes.tooltip }};
}

68
pygal/css/graph.css

@ -18,30 +18,7 @@
* along with pygal. If not, see <http://www.gnu.org/licenses/>.
*/
svg {
background-color: {{ style.background }};
}
svg * {
-webkit-transition: {{ style.transition }};
-moz-transition: {{ style.transition }};
transition: {{ style.transition }};
}
.graph > .background {
fill: {{ style.background }};
}
.plot > .background {
fill: {{ style.plot_background }};
}
.graph {
fill: {{ style.foreground }};
}
text.no_data {
fill: {{ style.foreground_light }};
text-anchor: middle;
}
@ -54,29 +31,14 @@ text.no_data {
}
.title {
fill: {{ style.foreground_light }};
font-size: {{ font_sizes.title }};
text-anchor: middle;
}
.legends .legend text {
font-family: monospace;
font-size: {{ font_sizes.legend }};
fill: {{ style.foreground }};
fill-opacity: 1;
}
.legends .legend:hover text {
fill: {{ style.foreground_light }};
}
.axis text {
font-size: {{ font_sizes.label }};
font-family: sans;
}
.axis.x text {
font-family: monospace;
text-anchor: middle;
}
@ -85,7 +47,6 @@ text.no_data {
}
.axis.y text {
font-family: monospace;
text-anchor: end;
}
@ -93,26 +54,19 @@ text.no_data {
font-size: 50%;
}
.axis .line {
stroke: {{ style.foreground_light }};
}
.axis .guide.line {
stroke: {{ style.foreground_dark }};
stroke-dasharray: 4,4;
}
.axis .major.line {
stroke: {{ style.foreground }};
stroke-dasharray: 6,6;
}
.axis text.major {
stroke-width: 0.5px;
stroke: {{ style.foreground_light }};
fill: {{ style.foreground_light }};
}
.axis.{{ hidden }} .guide.line {
.horizontal .axis.y .guide.line,
.vertical .axis.x .guide.line {
opacity: 0;
}
@ -120,12 +74,10 @@ text.no_data {
.line-graph .axis.x .guides:hover .guide.line,
.stackedline-graph .axis.x .guides:hover .guide.line,
.xy-graph .axis.x .guides:hover .guide.line {
stroke: {{ style.foreground_light }};
opacity: 1;
}
.axis .guides:hover text {
fill: {{ style.foreground_light }};
opacity: 1;
}
@ -133,14 +85,6 @@ text.no_data {
fill: none;
}
.reactive {
fill-opacity: {{ style.opacity }};
}
.reactive.active, .active .reactive {
fill-opacity: {{ style.opacity_hover }};
}
.dot {
stroke-width: 1px;
fill-opacity: 1;
@ -151,9 +95,7 @@ text.no_data {
}
.series text {
font-size: {{ font_sizes.value }};
stroke: none;
fill: {{ style.foreground_light }};
}
.series text.active {
@ -162,14 +104,10 @@ text.no_data {
#tooltip rect {
fill-opacity: 0.8;
fill: {{ style.plot_background }};
stroke: {{ style.foreground_light }};
}
#tooltip text {
fill-opacity: 1;
fill: {{ style.foreground_light }};
font-size: {{ font_sizes.tooltip }};
}
#tooltip text tspan.label {
@ -179,5 +117,3 @@ text.no_data {
a:visited {
fill: none;
}
{{ style.colors }}

112
pygal/css/style.css

@ -0,0 +1,112 @@
/*
* This file is part of pygal
*
* A python svg graph plotting library
* Copyright © 2012 Kozea
* This library is free software: you can redistribute it and/or modify it under
* the terms of the GNU Lesser General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This library is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
* details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with pygal. If not, see <http://www.gnu.org/licenses/>.
*/
/*
* Styles from config
*/
svg {
background-color: {{ style.background }};
}
svg * {
-webkit-transition: {{ style.transition }};
-moz-transition: {{ style.transition }};
transition: {{ style.transition }};
}
.graph > .background {
fill: {{ style.background }};
}
.plot > .background {
fill: {{ style.plot_background }};
}
.graph {
fill: {{ style.foreground }};
}
text.no_data {
fill: {{ style.foreground_light }};
}
.title {
fill: {{ style.foreground_light }};
}
.legends .legend text {
fill: {{ style.foreground }};
}
.legends .legend:hover text {
fill: {{ style.foreground_light }};
}
.axis .line {
stroke: {{ style.foreground_light }};
}
.axis .guide.line {
stroke: {{ style.foreground_dark }};
}
.axis .major.line {
stroke: {{ style.foreground }};
}
.axis text.major {
stroke: {{ style.foreground_light }};
fill: {{ style.foreground_light }};
}
.axis.y .guides:hover .guide.line,
.line-graph .axis.x .guides:hover .guide.line,
.stackedline-graph .axis.x .guides:hover .guide.line,
.xy-graph .axis.x .guides:hover .guide.line {
stroke: {{ style.foreground_light }};
}
.axis .guides:hover text {
fill: {{ style.foreground_light }};
}
.reactive {
fill-opacity: {{ style.opacity }};
}
.reactive.active, .active .reactive {
fill-opacity: {{ style.opacity_hover }};
}
.series text {
fill: {{ style.foreground_light }};
}
#tooltip rect {
fill: {{ style.plot_background }};
stroke: {{ style.foreground_light }};
}
#tooltip text {
fill: {{ style.foreground_light }};
}
{{ style.colors }}

4
pygal/graph/bar.py

@ -68,9 +68,9 @@ class Bar(Graph):
padding = .1 * width
inner_width = width - 2 * padding
if self.horizontal:
height = self.view.x(0) - y
height = self.view.x(self.zero) - y
else:
height = self.view.y(0) - y
height = self.view.y(self.zero) - y
if stack_vals == None:
bar_width = inner_width / len(self.series)
bar_padding = .1 * bar_width

14
pygal/graph/base.py

@ -23,7 +23,7 @@ Base for pygal charts
from __future__ import division
import io
from pygal.serie import Serie, Value
from pygal.serie import Serie, Value, PositiveValue
from pygal.view import Margin, Box
from pygal.util import get_text_box, get_texts_box, cut, rad, humanize
from pygal.svg import Svg
@ -60,6 +60,9 @@ class BaseGraph(object):
"""(Re-)Init the graph"""
self.margin = Margin(*([20] * 4))
self._box = Box()
if self.logarithmic and self.zero == 0:
# If logarithmic, default zero to 1
self.zero = 1
def __getattr__(self, attr):
"""Search in config, then in self"""
@ -129,6 +132,7 @@ class BaseGraph(object):
def _draw(self):
"""Draw all the things"""
self._prepare_data()
self._compute()
self._compute_margin()
self._decorate()
@ -142,6 +146,14 @@ class BaseGraph(object):
return False
return True
def _prepare_data(self):
"""Remove aberrant values"""
if self.logarithmic:
for serie in self.series:
for metadata in serie.metadata:
if metadata.value <= 0:
metadata.value = None
def _uniformize_data(self):
"""Make all series to max len"""
for serie in self.series:

4
pygal/graph/graph.py

@ -55,7 +55,9 @@ class Graph(BaseGraph):
def _make_graph(self):
"""Init common graph svg structure"""
self.nodes['graph'] = self.svg.node(
class_='graph %s-graph' % self.__class__.__name__.lower())
class_='graph %s-graph %s' % (
self.__class__.__name__.lower(),
'horizontal' if self.horizontal else 'vertical'))
self.svg.node(self.nodes['graph'], 'rect',
class_='background',
x=0, y=0,

2
pygal/serie.py

@ -50,7 +50,7 @@ class PositiveValue(Value):
"""Positive or zero value container"""
def __init__(self, value):
super(PositiveValue, self).__init__(max(0, value))
super(PositiveValue, self).__init__(max(value, 0))
class Label(object):

57
pygal/svg.py

@ -27,6 +27,7 @@ import os
import json
from lxml import etree
from math import cos, sin, pi
from urlparse import urlparse
from pygal.util import template, coord_format
from pygal import __version__
@ -37,6 +38,8 @@ class Svg(object):
def __init__(self, graph):
self.graph = graph
self.processing_instructions = [
etree.PI('xml', "version='1.0' encoding='utf-8'")]
self.root = None
self.defs = None
@ -47,24 +50,30 @@ class Svg(object):
nsmap={
None: self.ns,
'xlink': 'http://www.w3.org/1999/xlink',
},
# onload="svg_load(evt);"
)
})
self.root.append(etree.Comment(
u'Generated with pygal %s ©Kozea 2012' % __version__))
self.root.append(etree.Comment(u'http://github.com/Kozea/pygal'))
self.defs = self.node(tag='defs')
def add_style(self, css):
def add_styles(self):
"""Add the css to the svg"""
style = self.node(self.defs, 'style', type='text/css')
with io.open(css, encoding='utf-8') as f:
templ = template(
f.read(),
style=self.graph.style,
font_sizes=self.graph.font_sizes(),
hidden='y' if self.graph.horizontal else 'x')
style.text = templ
for css in ['base.css'] + list(self.graph.css):
if urlparse(css).scheme:
self.processing_instructions.append(
etree.PI(
'xml-stylesheet', 'href="%s"' % css))
else:
if not os.path.exists(css):
css = os.path.join(
os.path.dirname(__file__), 'css', css)
with io.open(css, encoding='utf-8') as f:
templ = template(
f.read(),
style=self.graph.style,
font_sizes=self.graph.font_sizes())
self.node(
self.defs, 'style', type='text/css').text = templ
def add_scripts(self):
"""Add the js to the svg"""
@ -72,13 +81,14 @@ class Svg(object):
common_script.text = " = ".join(
("window.config", json.dumps(self.graph.config.to_dict())))
for external_js in self.graph.external_js:
self.node(
self.defs, 'script', type='text/javascript', href=external_js)
for included_js in self.graph.included_js:
script = self.node(self.defs, 'script', type='text/javascript')
with io.open(included_js, encoding='utf-8') as f:
script.text = f.read()
for js in self.graph.js:
if urlparse(js).scheme:
self.node(
self.defs, 'script', type='text/javascript', href=js)
else:
script = self.node(self.defs, 'script', type='text/javascript')
with io.open(js, encoding='utf-8') as f:
script.text = f.read()
def node(self, parent=None, tag='g', attrib=None, **extras):
"""Make a new svg node"""
@ -160,8 +170,7 @@ class Svg(object):
def pre_render(self, no_data=False):
"""Last things to do before rendering"""
self.add_style(self.graph.base_css or os.path.join(
os.path.dirname(__file__), 'css', 'graph.css'))
self.add_styles()
self.add_scripts()
self.root.set(
'viewBox', '0 0 %d %d' % (self.graph.width, self.graph.height))
@ -179,8 +188,12 @@ class Svg(object):
"""Last thing to do before rendering"""
svg = etree.tostring(
self.root, pretty_print=True,
xml_declaration=not self.graph.disable_xml_declaration,
xml_declaration=False,
encoding='utf-8')
if not self.graph.disable_xml_declaration:
svg = '\n'.join(
[etree.tostring(pi) for pi in self.processing_instructions]
) + '\n' + svg
if self.graph.disable_xml_declaration or is_unicode:
svg = svg.decode('utf-8')
return svg

31
pygal/test/__init__.py

@ -16,3 +16,34 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
import pygal
from pygal.util import cut
def get_data(i):
return [
[(-1, 1), (2, 0), (0, 4)],
[(0, 1), (None, 2), (3, 2)],
[(-3, 3), (1, 3), (1, 1)],
[(1, 1), (1, 1), (1, 1)],
[(3, 2), (2, 1), (1, 1)]][i]
def pytest_generate_tests(metafunc):
if "Chart" in metafunc.funcargnames:
metafunc.parametrize("Chart", pygal.CHARTS)
if "datas" in metafunc.funcargnames:
metafunc.parametrize(
"datas",
[
[("Serie %d" % i, get_data(i)) for i in range(s)]
for s in (5, 1, 0)
])
def make_data(chart, datas):
for data in datas:
chart.add(data[0],
data[1] if chart.__class__ == pygal.XY else cut(data[1]))
return chart

3
pygal/test/test_config.py

@ -16,8 +16,9 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
from pygal import Line
from pygal import Line, Dot, Pie, Radar
from pygal.test.utils import texts
from pygal.test import pytest_generate_tests, make_data
def test_logarithmic():

61
pygal/test/test_graph.py

@ -17,38 +17,34 @@
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
import os
from pygal import Line
import pygal
import uuid
from pygal.util import cut
from pygal.test import pytest_generate_tests, make_data
def test_multi_render():
for Chart in pygal.CHARTS:
chart = Chart()
rng = range(20)
if Chart == pygal.XY:
rng = zip(rng, rng)
chart.add('Serie', rng)
chart.add('Serie 2', list(reversed(rng)))
svg = chart.render()
for i in range(2):
assert svg == chart.render()
def test_multi_render(Chart, datas):
chart = Chart()
chart = make_data(chart, datas)
svg = chart.render()
for i in range(2):
assert svg == chart.render()
def test_render_to_file():
def test_render_to_file(Chart, datas):
file_name = '/tmp/test_graph-%s.svg' % uuid.uuid4()
if os.path.exists(file_name):
os.remove(file_name)
line = Line()
line.add('Serie 1', [1])
line.render_to_file(file_name)
chart = Chart()
chart = make_data(chart, datas)
chart.render_to_file(file_name)
with open(file_name) as f:
assert 'pygal' in f.read()
os.remove(file_name)
def test_render_to_png():
def test_render_to_png(Chart, datas):
try:
import cairosvg
except ImportError:
@ -58,9 +54,34 @@ def test_render_to_png():
if os.path.exists(file_name):
os.remove(file_name)
line = Line()
line.add('Serie 1', [1])
line.render_to_png(file_name)
chart = Chart()
chart = make_data(chart, datas)
chart.render_to_png(file_name)
with open(file_name, 'rb') as f:
assert f.read()
os.remove(file_name)
def test_metadata(Chart):
chart = Chart()
v = range(7)
if Chart == pygal.XY:
v = map(lambda x: (x, x + 1), v)
chart.add('Serie with metadata', [
v[0],
{'value': v[1]},
{'value': v[2], 'label': 'Three'},
{'value': v[3], 'xlink': 'http://4.example.com/'},
{'value': v[4], 'xlink': 'http://5.example.com/', 'label': 'Five'},
{'value': v[5], 'xlink': {
'href': 'http://6.example.com/'}, 'label': 'Six'},
{'value': v[6], 'xlink': {
'href': 'http://7.example.com/',
'target': '_blank'}, 'label': 'Seven'}
])
q = chart.render_pyquery()
for md in (
'Three', 'http://4.example.com/',
'Five', 'http://7.example.com/', 'Seven'):
assert md in cut(q('desc'), 'text')

4
pygal/util.py

@ -123,6 +123,8 @@ ident = lambda x: x
def compute_logarithmic_scale(min_, max_):
"""Compute an optimal scale for logarithmic"""
if max_ <= 0 or min_ <= 0:
return []
min_order = int(floor(log10(min_)))
max_order = int(ceil(log10(max_)))
positions = []
@ -199,6 +201,8 @@ def decorate(svg, node, metadata):
for key in dir(metadata):
if key not in ('value') and not key.startswith('_'):
value = getattr(metadata, key)
if key == 'xlink' and isinstance(value, dict):
value = value.get('href', value)
if value:
svg.node(node, 'desc', class_=key).text = str(value)
return node

Loading…
Cancel
Save