diff --git a/CHANGELOG b/CHANGELOG
index 0ec9c89e..ac6ac157 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,6 @@
+V 1.5.0 UNRELEASED
+ Add per serie configuration
+
V 1.4.6
Add support for \n separated multiline titles (thanks sirlark)
New show_only_major_dots option (thanks Le-Stagiaire)
diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py
index 372f759d..1ae3655d 100644
--- a/demo/moulinrouge/tests.py
+++ b/demo/moulinrouge/tests.py
@@ -439,4 +439,28 @@ def test_major_dots():
line.x_labels_major = ['lol3']
return line.render_response()
+ @app.route('/test/stroke_config')
+ def test_stroke_config():
+ line = Line()
+ line.add('test_no_line', range(12), stroke=False)
+ line.add('test', reversed(range(12)))
+ line.add('test_no_dots', [5] * 12, show_dots=False)
+ line.add('test_big_dots', [
+ randint(1, 12) for _ in range(12)], dots_size=5)
+ line.add('test_fill', [
+ randint(1, 3) for _ in range(12)], fill=True)
+
+ line.x_labels = [
+ 'lol', 'lol1', 'lol2', 'lol3', 'lol4', 'lol5',
+ 'lol6', 'lol7', 'lol8', 'lol9', 'lol10', 'lol11']
+ return line.render_response()
+
+ @app.route('/test/pie_serie_radius')
+ def test_pie_serie_radius():
+ pie = Pie()
+ for i in range(10):
+ pie.add(str(i), i, inner_radius=(10 - i) / 10)
+
+ return pie.render_response()
+
return filter(lambda x: x.startswith('test'), locals())
diff --git a/pygal/config.py b/pygal/config.py
index 08f23c97..753402fa 100644
--- a/pygal/config.py
+++ b/pygal/config.py
@@ -46,7 +46,7 @@ def __init__(
self.subdoc = subdoc
self.subtype = subtype
self.name = "Unbound"
- if not category in self._categories:
+ if category not in self._categories:
self._categories.append(category)
CONFIG_ITEMS.append(self)
@@ -101,7 +101,84 @@ def __new__(mcs, classname, bases, classdict):
return type.__new__(mcs, classname, bases, classdict)
-class Config(MetaConfig('ConfigBase', (object,), {})):
+class BaseConfig(MetaConfig('ConfigBase', (object,), {})):
+
+ def __init__(self, **kwargs):
+ """Can be instanciated with config kwargs"""
+ for k in dir(self):
+ v = getattr(self, k)
+ if (k not in self.__dict__ and not
+ k.startswith('_') and not
+ hasattr(v, '__call__')):
+ if isinstance(v, Key):
+ if v.is_list and v.value is not None:
+ v = list(v.value)
+ else:
+ v = v.value
+ setattr(self, k, v)
+ self._update(kwargs)
+
+ def __call__(self, **kwargs):
+ """Can be updated with kwargs"""
+ self._update(kwargs)
+
+ def _update(self, kwargs):
+ self.__dict__.update(
+ dict([(k, v) for (k, v) in kwargs.items()
+ if not k.startswith('_') and k in dir(self)]))
+
+ def font_sizes(self, with_unit=True):
+ """Getter for all font size configs"""
+ fs = FontSizes()
+ for name in dir(self):
+ if name.endswith('_font_size'):
+ setattr(
+ fs,
+ name.replace('_font_size', ''),
+ ('%dpx' % getattr(self, name))
+ if with_unit else getattr(self, name))
+ return fs
+
+ def to_dict(self):
+ config = {}
+ for attr in dir(self):
+ if not attr.startswith('__'):
+ value = getattr(self, attr)
+ if hasattr(value, 'to_dict'):
+ config[attr] = value.to_dict()
+ elif not hasattr(value, '__call__'):
+ config[attr] = value
+ return config
+
+ def copy(self):
+ return deepcopy(self)
+
+
+class CommonConfig(BaseConfig):
+ stroke = Key(
+ True, bool, "Look",
+ "Line dots (set it to false to get a scatter plot)")
+
+ show_dots = Key(True, bool, "Look", "Set to false to remove dots")
+
+ show_only_major_dots = Key(
+ False, bool, "Look",
+ "Set to true to show only major dots according to their majored label")
+
+ dots_size = Key(2.5, float, "Look", "Radius of the dots")
+
+ fill = Key(
+ False, bool, "Look", "Fill areas under lines")
+
+ rounded_bars = Key(
+ None, int, "Look",
+ "Set this to the desired radius in px (for Bar-like charts)")
+
+ inner_radius = Key(
+ 0, float, "Look", "Piechart inner radius (donut), must be <.9")
+
+
+class Config(CommonConfig):
"""Class holding config values"""
style = Key(
@@ -113,7 +190,7 @@ class Config(MetaConfig('ConfigBase', (object,), {})):
"It can be an absolute file path or an external link",
str)
- ############ Look ############
+ # Look #
title = Key(
None, str, "Look",
"Graph title.", "Leave it to None to disable title.")
@@ -138,21 +215,6 @@ class Config(MetaConfig('ConfigBase', (object,), {})):
show_y_guides = Key(True, bool, "Look",
"Set to false to hide y guide lines")
- show_dots = Key(True, bool, "Look", "Set to false to remove dots")
-
- show_only_major_dots = Key(
- False, bool, "Look",
- "Set to true to show only major dots according to their majored label")
-
- dots_size = Key(2.5, float, "Look", "Radius of the dots")
-
- stroke = Key(
- True, bool, "Look",
- "Line dots (set it to false to get a scatter plot)")
-
- fill = Key(
- False, bool, "Look", "Fill areas under lines")
-
show_legend = Key(
True, bool, "Look", "Set to false to remove legend")
@@ -162,9 +224,6 @@ class Config(MetaConfig('ConfigBase', (object,), {})):
legend_box_size = Key(
12, int, "Look", "Size of legend boxes")
- rounded_bars = Key(
- None, int, "Look", "Set this to the desired radius in px")
-
spacing = Key(
10, int, "Look",
"Space between titles/legend/axes")
@@ -175,10 +234,7 @@ class Config(MetaConfig('ConfigBase', (object,), {})):
tooltip_border_radius = Key(0, int, "Look", "Tooltip border radius")
- inner_radius = Key(
- 0, float, "Look", "Piechart inner radius (donut), must be <.9")
-
- ############ Label ############
+ # Label #
x_labels = Key(
None, list, "Label",
"X labels, must have same len than data.",
@@ -235,7 +291,7 @@ class Config(MetaConfig('ConfigBase', (object,), {})):
"%Y-%m-%d %H:%M:%S.%f", str, "Label",
"Date format for strftime to display the DateY X labels")
- ############ Value ############
+ # Value #
human_readable = Key(
False, bool, "Value", "Display values in human readable format",
"(ie: 12.4M)")
@@ -274,7 +330,7 @@ class Config(MetaConfig('ConfigBase', (object,), {})):
"Set the ordinate zero value",
"Useful for filling to another base than abscissa")
- ############ Text ############
+ # Text #
no_data_text = Key(
"No data", str, "Text", "Text to display when no data is given")
@@ -308,7 +364,7 @@ class Config(MetaConfig('ConfigBase', (object,), {})):
None, int, "Text",
"Label string length truncation threshold", "None = auto")
- ############ Misc ############
+ # Misc #
js = Key(
('http://kozea.github.com/pygal.js/javascripts/svg.jquery.js',
'http://kozea.github.com/pygal.js/javascripts/pygal-tooltips.js'),
@@ -335,52 +391,10 @@ class Config(MetaConfig('ConfigBase', (object,), {})):
False, bool, "Misc",
"Don't prefix css")
- def __init__(self, **kwargs):
- """Can be instanciated with config kwargs"""
- for k in dir(self):
- v = getattr(self, k)
- if (k not in self.__dict__ and not
- k.startswith('_') and not
- hasattr(v, '__call__')):
- if isinstance(v, Key):
- v = v.value
- setattr(self, k, v)
- self.css = list(self.css)
- self.js = list(self.js)
- self._update(kwargs)
+class SerieConfig(CommonConfig):
+ """Class holding serie config values"""
- def __call__(self, **kwargs):
- """Can be updated with kwargs"""
- self._update(kwargs)
-
- def _update(self, kwargs):
- self.__dict__.update(
- dict([(k, v) for (k, v) in kwargs.items()
- if not k.startswith('_') and k in dir(self)]))
-
- def font_sizes(self, with_unit=True):
- """Getter for all font size configs"""
- fs = FontSizes()
- for name in dir(self):
- if name.endswith('_font_size'):
- setattr(
- fs,
- name.replace('_font_size', ''),
- ('%dpx' % getattr(self, name))
- if with_unit else getattr(self, name))
- return fs
-
- def to_dict(self):
- config = {}
- for attr in dir(self):
- if not attr.startswith('__'):
- value = getattr(self, attr)
- if hasattr(value, 'to_dict'):
- config[attr] = value.to_dict()
- elif not hasattr(value, '__call__'):
- config[attr] = value
- return config
-
- def copy(self):
- return deepcopy(self)
+ secondary = Key(
+ False, bool, "Misc",
+ "Set it to put the serie in a second axis")
diff --git a/pygal/ghost.py b/pygal/ghost.py
index abd9dee0..d0ebd53c 100644
--- a/pygal/ghost.py
+++ b/pygal/ghost.py
@@ -26,9 +26,9 @@
from __future__ import division
import io
import sys
-from pygal.config import Config
from pygal._compat import u, is_list_like
from pygal.graph import CHARTS_NAMES
+from pygal.config import Config, SerieConfig
from pygal.util import prepare_values
from uuid import uuid4
@@ -73,14 +73,14 @@ def __init__(self, config=None, **kwargs):
self.raw_series2 = []
self.xml_filters = []
- def add(self, title, values, secondary=False):
+ def add(self, title, values, **kwargs):
"""Add a serie to this graph"""
if not is_list_like(values) and not isinstance(values, dict):
values = [values]
- if secondary:
- self.raw_series2.append((title, values))
+ if kwargs.get('secondary', False):
+ self.raw_series2.append((title, values, kwargs))
else:
- self.raw_series.append((title, values))
+ self.raw_series.append((title, values, kwargs))
def add_xml_filter(self, callback):
self.xml_filters.append(callback)
diff --git a/pygal/graph/bar.py b/pygal/graph/bar.py
index a02c71df..26abb15e 100644
--- a/pygal/graph/bar.py
+++ b/pygal/graph/bar.py
@@ -36,7 +36,8 @@ def __init__(self, *args, **kwargs):
self._x_ranges = None
super(Bar, self).__init__(*args, **kwargs)
- def _bar(self, parent, x, y, index, i, zero, shift=True, secondary=False):
+ def _bar(self, parent, x, y, index, i, zero,
+ shift=True, secondary=False, rounded=False):
width = (self.view.x(1) - self.view.x(0)) / self._len
x, y = self.view((x, y))
series_margin = width * self._series_margin
@@ -49,7 +50,7 @@ def _bar(self, parent, x, y, index, i, zero, shift=True, secondary=False):
x += serie_margin
width -= 2 * serie_margin
height = self.view.y(zero) - y
- r = self.rounded_bars * 1 if self.rounded_bars else 0
+ r = rounded * 1 if rounded else 0
self.svg.transposable_node(
parent, 'rect',
x=x, y=y, rx=r, ry=r, width=width, height=height,
@@ -79,7 +80,8 @@ def bar(self, serie_node, serie, index, rescale=False):
val = self._format(serie.values[i])
x_center, y_center = self._bar(
- bar, x, y, index, i, self.zero, secondary=rescale)
+ bar, x, y, index, i, self.zero, secondary=rescale,
+ rounded=serie.rounded_bars)
self._tooltip_data(
bar, val, x_center, y_center, classes="centered")
self._static_value(serie_node, val, x_center, y_center)
diff --git a/pygal/graph/histogram.py b/pygal/graph/histogram.py
index 16c2f007..7db4175e 100644
--- a/pygal/graph/histogram.py
+++ b/pygal/graph/histogram.py
@@ -67,10 +67,11 @@ def _has_data(self):
"""Check if there is any data"""
return sum(
map(len, map(lambda s: s.safe_values, self.series))) != 0 and any((
- sum(map(abs, self.xvals)) != 0,
- sum(map(abs, self.yvals)) != 0))
+ sum(map(abs, self.xvals)) != 0,
+ sum(map(abs, self.yvals)) != 0))
- def _bar(self, parent, x0, x1, y, index, i, zero, secondary=False):
+ def _bar(self, parent, x0, x1, y, index, i, zero,
+ secondary=False, rounded=False):
x, y = self.view((x0, y))
x1, _ = self.view((x1, y))
width = x1 - x
@@ -104,7 +105,8 @@ def bar(self, serie_node, serie, index, rescale=False):
val = self._format(serie.values[i][0])
x_center, y_center = self._bar(
- bar, x0, x1, y, index, i, self.zero, secondary=rescale)
+ bar, x0, x1, y, index, i, self.zero, secondary=rescale,
+ rounded=serie.rounded_bars)
self._tooltip_data(
bar, val, x_center, y_center, classes="centered")
self._static_value(serie_node, val, x_center, y_center)
diff --git a/pygal/graph/line.py b/pygal/graph/line.py
index bf34ef6e..cc69bdd2 100644
--- a/pygal/graph/line.py
+++ b/pygal/graph/line.py
@@ -66,8 +66,8 @@ def line(self, serie_node, serie, rescale=False):
else:
points = serie.points
view_values = list(map(self.view, points))
- if self.show_dots:
- if self.show_only_major_dots:
+ if serie.show_dots:
+ if serie.show_only_major_dots:
major_dots_index = []
if self.x_labels:
if self.x_labels_major:
@@ -88,7 +88,7 @@ def line(self, serie_node, serie, rescale=False):
0, len(self.x_labels), self.x_labels_major_every)
for i, (x, y) in enumerate(view_values):
- if None in (x, y) or (self.show_only_major_dots
+ if None in (x, y) or (serie.show_only_major_dots
and i not in major_dots_index):
continue
metadata = serie.metadata.get(i)
@@ -103,7 +103,7 @@ def line(self, serie_node, serie, rescale=False):
self.svg.node(serie_node['overlay'], class_="dots"),
metadata)
val = self._get_value(serie.points, i)
- self.svg.node(dots, 'circle', cx=x, cy=y, r=self.dots_size,
+ self.svg.node(dots, 'circle', cx=x, cy=y, r=serie.dots_size,
class_='dot reactive tooltip-trigger')
self._tooltip_data(
dots, val, x, y)
@@ -112,14 +112,14 @@ def line(self, serie_node, serie, rescale=False):
x + self.value_font_size,
y + self.value_font_size)
- if self.stroke:
+ if serie.stroke:
if self.interpolate:
view_values = list(map(self.view, serie.interpolated))
- if self.fill:
+ if serie.fill:
view_values = self._fill(view_values)
self.svg.line(
serie_node['plot'], view_values, close=self._self_close,
- class_='line reactive' + (' nofill' if not self.fill else ''))
+ class_='line reactive' + (' nofill' if not serie.fill else ''))
def _compute(self):
# X Labels
diff --git a/pygal/graph/pie.py b/pygal/graph/pie.py
index f9c216d0..54e9159f 100644
--- a/pygal/graph/pie.py
+++ b/pygal/graph/pie.py
@@ -59,7 +59,7 @@ def slice(self, serie_node, start_angle, serie, total):
big_radius = radius
else:
big_radius = radius * .9
- small_radius = radius * self.config.inner_radius
+ small_radius = radius * serie.inner_radius
self.svg.slice(
serie_node, slice_, big_radius, small_radius,
diff --git a/pygal/graph/stackedbar.py b/pygal/graph/stackedbar.py
index 8bd129c0..2b22e656 100644
--- a/pygal/graph/stackedbar.py
+++ b/pygal/graph/stackedbar.py
@@ -90,7 +90,8 @@ def _compute(self):
self._secondary_max = (positive_vals and max(
sum_(max(positive_vals)), self.zero)) or self.zero
- def _bar(self, parent, x, y, index, i, zero, shift=False, secondary=False):
+ def _bar(self, parent, x, y, index, i, zero,
+ shift=False, secondary=False, rounded=False):
if secondary:
cumulation = (self.secondary_negative_cumulation
if y < self.zero else
@@ -118,7 +119,7 @@ def _bar(self, parent, x, y, index, i, zero, shift=False, secondary=False):
x += serie_margin
width -= 2 * serie_margin
height = self.view.y(zero) - y
- r = self.rounded_bars * 1 if self.rounded_bars else 0
+ r = rounded * 1 if rounded else 0
self.svg.transposable_node(
parent, 'rect',
x=x, y=y, rx=r, ry=r, width=width, height=height,
diff --git a/pygal/graph/verticalpyramid.py b/pygal/graph/verticalpyramid.py
index 7c52ac90..6db4860b 100644
--- a/pygal/graph/verticalpyramid.py
+++ b/pygal/graph/verticalpyramid.py
@@ -81,8 +81,9 @@ def _compute_secondary(self):
y)
for y in y_pos]
- def _bar(self, parent, x, y, index, i, zero, shift=True, secondary=False):
+ def _bar(self, parent, x, y, index, i, zero,
+ shift=True, secondary=False, rounded=False):
if index % 2:
y = -y
return super(VerticalPyramid, self)._bar(
- parent, x, y, index, i, zero, False, secondary)
+ parent, x, y, index, i, zero, False, secondary, rounded)
diff --git a/pygal/serie.py b/pygal/serie.py
index d3636b57..f48479c9 100644
--- a/pygal/serie.py
+++ b/pygal/serie.py
@@ -25,9 +25,11 @@
class Serie(object):
"""Serie containing title, values and the graph serie index"""
- def __init__(self, title, values, metadata=None):
+ def __init__(self, title, values, config, metadata=None):
self.title = title
self.values = values
+ self.config = config
+ self.__dict__.update(config.to_dict())
self.metadata = metadata or {}
@cached_property
diff --git a/pygal/test/test_config.py b/pygal/test/test_config.py
index 6510caec..b8ee2539 100644
--- a/pygal/test/test_config.py
+++ b/pygal/test/test_config.py
@@ -16,6 +16,7 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see .
+
from pygal import (
Line, Dot, Pie, Radar, Config, Bar, Funnel, Worldmap,
SupranationalWorldmap, Histogram, Gauge, Box,
diff --git a/pygal/test/test_serie_config.py b/pygal/test/test_serie_config.py
new file mode 100644
index 00000000..f2f09691
--- /dev/null
+++ b/pygal/test/test_serie_config.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+# This file is part of pygal
+#
+# A python svg graph plotting library
+# Copyright © 2012-2014 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 .
+
+from pygal.test import pytest_generate_tests
+from pygal import Line
+
+
+def test_serie_config():
+ s1 = [1, 3, 12, 3, 4]
+ s2 = [7, -4, 10, None, 8, 3, 1]
+
+ chart = Line()
+ chart.add('1', s1)
+ chart.add('2', s2)
+ q = chart.render_pyquery()
+ assert len(q('.serie-0 .line')) == 1
+ assert len(q('.serie-1 .line')) == 1
+ assert len(q('.serie-0 .dot')) == 5
+ assert len(q('.serie-1 .dot')) == 6
+
+ chart = Line(stroke=False)
+ chart.add('1', s1)
+ chart.add('2', s2)
+ q = chart.render_pyquery()
+ assert len(q('.serie-0 .line')) == 0
+ assert len(q('.serie-1 .line')) == 0
+ assert len(q('.serie-0 .dot')) == 5
+ assert len(q('.serie-1 .dot')) == 6
+
+ chart = Line()
+ chart.add('1', s1, stroke=False)
+ chart.add('2', s2)
+ q = chart.render_pyquery()
+ assert len(q('.serie-0 .line')) == 0
+ assert len(q('.serie-1 .line')) == 1
+ assert len(q('.serie-0 .dot')) == 5
+ assert len(q('.serie-1 .dot')) == 6
+
+ chart = Line(stroke=False)
+ chart.add('1', s1, stroke=True)
+ chart.add('2', s2)
+ q = chart.render_pyquery()
+ assert len(q('.serie-0 .line')) == 1
+ assert len(q('.serie-1 .line')) == 0
+ assert len(q('.serie-0 .dot')) == 5
+ assert len(q('.serie-1 .dot')) == 6
diff --git a/pygal/util.py b/pygal/util.py
index a0608a9f..4607cdee 100644
--- a/pygal/util.py
+++ b/pygal/util.py
@@ -318,11 +318,11 @@ def safe_enumerate(iterable):
if v is not None:
yield i, v
-from pygal.serie import Serie
-
def prepare_values(raw, config, cls):
"""Prepare the values to start with sane values"""
+ from pygal.serie import Serie
+ from pygal.config import SerieConfig
from pygal.graph.datey import DateY
from pygal.graph.histogram import Histogram
from pygal.graph.worldmap import Worldmap
@@ -349,13 +349,14 @@ def prepare_values(raw, config, cls):
raw = [(
title,
- list(raw_values) if not isinstance(raw_values, dict) else raw_values
- ) for title, raw_values in raw]
+ list(raw_values) if not isinstance(raw_values, dict) else raw_values,
+ serie_config_kwargs
+ ) for title, raw_values, serie_config_kwargs in raw]
- width = max([len(values) for _, values in raw] +
+ width = max([len(values) for _, values, _ in raw] +
[len(config.x_labels or [])])
- for title, raw_values in raw:
+ for title, raw_values, serie_config_kwargs in raw:
metadata = {}
values = []
if isinstance(raw_values, dict):
@@ -399,7 +400,10 @@ def prepare_values(raw, config, cls):
else:
value = adapter(value)
values.append(value)
- series.append(Serie(title, values, metadata))
+ serie_config = SerieConfig()
+ serie_config(**config.to_dict())
+ serie_config(**serie_config_kwargs)
+ series.append(Serie(title, values, serie_config, metadata))
return series