diff --git a/demo/simple_test.py b/demo/simple_test.py
index a1f320e..c2b2813 100755
--- a/demo/simple_test.py
+++ b/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"
diff --git a/pygal/config.py b/pygal/config.py
index 28bb737..0fd2410 100644
--- a/pygal/config.py
+++ b/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'
]
diff --git a/pygal/css/base.css b/pygal/css/base.css
new file mode 100644
index 0000000..5391838
--- /dev/null
+++ b/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 .
+*/
+
+/*
+ * 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 }};
+}
diff --git a/pygal/css/graph.css b/pygal/css/graph.css
index 668d0f5..6e65b12 100644
--- a/pygal/css/graph.css
+++ b/pygal/css/graph.css
@@ -18,30 +18,7 @@
* along with pygal. If not, see .
*/
-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 }}
diff --git a/pygal/css/style.css b/pygal/css/style.css
new file mode 100644
index 0000000..25824b9
--- /dev/null
+++ b/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 .
+*/
+
+/*
+ * 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 }}
diff --git a/pygal/graph/bar.py b/pygal/graph/bar.py
index d685227..96be243 100644
--- a/pygal/graph/bar.py
+++ b/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
diff --git a/pygal/graph/base.py b/pygal/graph/base.py
index b87f90c..9fa7f94 100644
--- a/pygal/graph/base.py
+++ b/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:
diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py
index 164542d..3b07461 100644
--- a/pygal/graph/graph.py
+++ b/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,
diff --git a/pygal/serie.py b/pygal/serie.py
index 176b2d3..0e5f36c 100644
--- a/pygal/serie.py
+++ b/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):
diff --git a/pygal/svg.py b/pygal/svg.py
index 12d15e3..d5b325b 100644
--- a/pygal/svg.py
+++ b/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
diff --git a/pygal/test/__init__.py b/pygal/test/__init__.py
index 98db20d..ac7e4f2 100644
--- a/pygal/test/__init__.py
+++ b/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 .
+
+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
diff --git a/pygal/test/test_config.py b/pygal/test/test_config.py
index daa55c7..ff9e04b 100644
--- a/pygal/test/test_config.py
+++ b/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 .
-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():
diff --git a/pygal/test/test_graph.py b/pygal/test/test_graph.py
index dc43e02..88309e9 100644
--- a/pygal/test/test_graph.py
+++ b/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 .
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')
diff --git a/pygal/util.py b/pygal/util.py
index 599fe76..5c3cc70 100644
--- a/pygal/util.py
+++ b/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