diff --git a/pygal/config.py b/pygal/config.py
index 08f23c9..9f225cc 100644
--- a/pygal/config.py
+++ b/pygal/config.py
@@ -126,6 +126,8 @@ class Config(MetaConfig('ConfigBase', (object,), {})):
None, str, "Look",
"Graph Y-Axis title.", "Leave it to None to disable Y-Axis title.")
+ y_errors = Key(False, bool, "Look", "Set to True to display y-errors.")
+
width = Key(
800, int, "Look", "Graph width")
diff --git a/pygal/css/style.css b/pygal/css/style.css
index bc8f31e..5784964 100644
--- a/pygal/css/style.css
+++ b/pygal/css/style.css
@@ -129,6 +129,10 @@
stroke-width: 10;
}
+{{ id }}.err_marks .errors {
+ stroke: {{ style.foreground_dark }};
+}
+
{{ colors }}
diff --git a/pygal/graph/#box.py# b/pygal/graph/#box.py#
new file mode 100644
index 0000000..de928e0
--- /dev/null
+++ b/pygal/graph/#box.py#
@@ -0,0 +1,208 @@
+# -*- 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 .
+"""
+Box plot
+"""
+
+from __future__ import division
+from pygal.graph.graph import Graph
+from pygal.util import compute_scale, decorate
+from pygal._compat import is_list_like
+
+
+class Box(Graph):
+ """
+ Box plot
+ For each series, shows the median value, the 25th and 75th percentiles,
+ and the values within
+ 1.5 times the interquartile range of the 25th and 75th percentiles.
+
+ See http://en.wikipedia.org/wiki/Box_plot
+ """
+ _series_margin = .06
+
+ def __init__(self, *args, **kwargs):
+ super(Box, self).__init__(*args, **kwargs)
+
+ @property
+ def _format(self):
+ """Return the value formatter for this graph"""
+ sup = super(Box, self)._format
+
+ def format_maybe_quartile(x):
+ if is_list_like(x):
+ if len(x) == 5:
+ return 'Q1: %s Q2: %s Q3: %s' % tuple(map(sup, x[1:4]))
+ else:
+ return sup(x)
+ return format_maybe_quartile
+
+ def _compute(self):
+ """
+ Compute parameters necessary for later steps
+ within the rendering process
+ """
+ for serie in self.series:
+ serie.values = self._box_points(serie.values)
+
+ if self._min:
+ self._box.ymin = min(self._min, self.zero)
+ if self._max:
+ self._box.ymax = max(self._max, self.zero)
+
+ x_pos = [
+ x / self._len for x in range(self._len + 1)
+ ] if self._len > 1 else [0, 1] # Center if only one value
+
+ self._points(x_pos)
+
+ y_pos = compute_scale(
+ self._box.ymin, self._box.ymax, self.logarithmic, self.order_min
+ ) if not self.y_labels else list(map(float, self.y_labels))
+
+ self._x_labels = self.x_labels and list(zip(self.x_labels, [
+ (i + .5) / self._order for i in range(self._order)]))
+ self._y_labels = list(zip(map(self._format, y_pos), y_pos))
+
+ def _plot(self):
+ """
+ Plot the series data
+ """
+ for index, serie in enumerate(self.series):
+ self._boxf(self._serie(index), serie, index)
+
+ def _boxf(self, serie_node, serie, index):
+ """
+ For a specific series, draw the box plot.
+ """
+ # Note: q0 and q4 do not literally mean the zero-th quartile
+ # and the fourth quartile, but rather the distance from 1.5 times
+ # the inter-quartile range to Q1 and Q3, respectively.
+ boxes = self.svg.node(serie_node['plot'], class_="boxes")
+
+ metadata = serie.metadata.get(0)
+
+ box = decorate(
+ self.svg,
+ self.svg.node(boxes, class_='box'),
+ metadata)
+ val = self._format(serie.values)
+
+ x_center, y_center = self._draw_box(box, serie.values, index)
+ self._tooltip_data(box, val, x_center, y_center, classes="centered")
+ self._static_value(serie_node, val, x_center, y_center)
+
+ def _draw_box(self, parent_node, quartiles, box_index):
+ """
+ Return the center of a bounding box defined by a box plot.
+ Draws a box plot on self.svg.
+ """
+ width = (self.view.x(1) - self.view.x(0)) / self._order
+ series_margin = width * self._series_margin
+ left_edge = self.view.x(0) + width * box_index + series_margin
+ width -= 2 * series_margin
+
+ # draw lines for whiskers - bottom, median, and top
+ for i, whisker in enumerate(
+ (quartiles[0], quartiles[2], quartiles[4])):
+ whisker_width = width if i == 1 else width / 2
+ shift = (width - whisker_width) / 2
+ xs = left_edge + shift
+ xe = left_edge + width - shift
+ self.svg.line(
+ parent_node,
+ coords=[(xs, self.view.y(whisker)),
+ (xe, self.view.y(whisker))],
+ class_='reactive tooltip-trigger',
+ attrib={'stroke-width': 3})
+
+ # draw lines connecting whiskers to box (Q1 and Q3)
+ self.svg.line(
+ parent_node,
+ coords=[(left_edge + width / 2, self.view.y(quartiles[0])),
+ (left_edge + width / 2, self.view.y(quartiles[1]))],
+ class_='reactive tooltip-trigger',
+ attrib={'stroke-width': 2})
+ self.svg.line(
+ parent_node,
+ coords=[(left_edge + width / 2, self.view.y(quartiles[4])),
+ (left_edge + width / 2, self.view.y(quartiles[3]))],
+ class_='reactive tooltip-trigger',
+ attrib={'stroke-width': 2})
+
+ # box, bounded by Q1 and Q3
+ self.svg.node(
+ parent_node,
+ tag='rect',
+ x=left_edge,
+ y=self.view.y(quartiles[1]),
+ height=self.view.y(quartiles[3]) - self.view.y(quartiles[1]),
+ width=width,
+ class_='subtle-fill reactive tooltip-trigger')
+
+ return (left_edge + width / 2, self.view.y(
+ sum(quartiles) / len(quartiles)))
+
+ @staticmethod
+ def _box_points(values):
+ """
+ Return a 5-tuple of Q1 - 1.5 * IQR, Q1, Median, Q3,
+ and Q3 + 1.5 * IQR for a list of numeric values.
+
+ The iterator values may include None values.
+
+ Uses quartile definition from Mendenhall, W. and
+ Sincich, T. L. Statistics for Engineering and the
+ Sciences, 4th ed. Prentice-Hall, 1995.
+ """
+ def median(seq):
+ n = len(seq)
+ if n % 2 == 0: # seq has an even length
+ return (seq[n // 2] + seq[n // 2 - 1]) / 2
+ else: # seq has an odd length
+ return seq[n // 2]
+
+ # sort the copy in case the originals must stay in original order
+ s = sorted([x for x in values if x is not None])
+ n = len(s)
+ if not n:
+ return 0, 0, 0, 0, 0
+ else:
+ q2 = median(s)
+ # See 'Method 3' in http://en.wikipedia.org/wiki/Quartile
+ if n % 2 == 0: # even
+ q1 = median(s[:n // 2])
+ q3 = median(s[n // 2:])
+ else: # odd
+ if n == 1: # special case
+ q1 = s[0]
+ q3 = s[0]
+ elif n % 4 == 1: # n is of form 4n + 1 where n >= 1
+ m = (n - 1) // 4
+ q1 = 0.25 * s[m-1] + 0.75 * s[m]
+ q3 = 0.75 * s[3*m] + 0.25 * s[3*m + 1]
+ else: # n is of form 4n + 3 where n >= 1
+ m = (n - 3) // 4
+ q1 = 0.75 * s[m] + 0.25 * s[m+1]
+ q3 = 0.25 * s[3*m+1] + 0.75 * s[3*m+2]
+
+ iqr = q3 - q1
+ q0 = q1 - 1.5 * iqr
+ q4 = q3 + 1.5 * iqr
+ return q0, q1, q2, q3, q4
diff --git a/pygal/graph/#pie.py# b/pygal/graph/#pie.py#
new file mode 100644
index 0000000..3e0826e
--- /dev/null
+++ b/pygal/graph/#pie.py#
@@ -0,0 +1,87 @@
+# -*- 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 .
+"""
+Pie chart
+
+"""
+
+from __future__ import division
+from pygal.util import decorate
+from pygal.graph.graph import Graph
+from pygal.adapters import positive, none_to_zero
+from math import pi
+
+
+class Pie(Graph):
+ """Pie graph"""
+
+ _adapters = [positive, none_to_zero]
+
+ def slice(self, serie_node, start_angle, serie, total):
+ """Make a serie slice"""
+ dual = self._len > 1 and not self._order == 1
+
+ slices = self.svg.node(serie_node['plot'], class_="slices")
+ serie_angle = 0
+ total_perc = 0
+ original_start_angle = start_angle
+ center = ((self.width - self.margin.x) / 2.,
+ (self.height - self.margin.y) / 2.)
+ radius = min(center)
+ for i, val in enumerate(serie.values):
+ perc = val / total
+ angle = 2 * pi * perc
+ serie_angle += angle
+ val = '{0:.2%}'.format(perc)
+ metadata = serie.metadata.get(i)
+ slice_ = decorate(
+ self.svg,
+ self.svg.node(slices, class_="slice"),
+ metadata)
+ if dual:
+ small_radius = radius * .9
+ big_radius = radius
+ else:
+ big_radius = radius * .9
+ small_radius = radius * self.config.inner_radius
+
+ self.svg.slice(
+ serie_node, slice_, big_radius, small_radius,
+ angle, start_angle, center, val)
+ start_angle += angle
+ total_perc += perc
+
+ if dual:
+ val = '{0:.2%}'.format(total_perc)
+ self.svg.slice(serie_node,
+ self.svg.node(slices, class_="big_slice"),
+ radius * .9, 0, serie_angle,
+ original_start_angle, center, val)
+ return serie_angle
+
+ def _plot(self):
+ total = sum(map(sum, map(lambda x: x.values, self.series)))
+
+ if total == 0:
+ return
+ current_angle = 0
+ for index, serie in enumerate(self.series):
+ angle = self.slice(
+ self._serie(index), current_angle, serie, total)
+ current_angle += angle
diff --git a/pygal/graph/bar.py b/pygal/graph/bar.py
index a02c71d..879f272 100644
--- a/pygal/graph/bar.py
+++ b/pygal/graph/bar.py
@@ -23,7 +23,8 @@ Bar chart
from __future__ import division
from pygal.graph.graph import Graph
-from pygal.util import swap, ident, compute_scale, decorate
+from pygal.util import compute_scale, decorate
+from pygal.serie import NestedSerie
class Bar(Graph):
@@ -36,8 +37,10 @@ class Bar(Graph):
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, errors_node, shift=True,
+ secondary=False, nested=None):
width = (self.view.x(1) - self.view.x(0)) / self._len
+
x, y = self.view((x, y))
series_margin = width * self._series_margin
x += series_margin
@@ -54,12 +57,17 @@ class Bar(Graph):
parent, 'rect',
x=x, y=y, rx=r, ry=r, width=width, height=height,
class_='rect reactive tooltip-trigger')
- transpose = swap if self.horizontal else ident
- return transpose((x + width / 2, y + height / 2))
+ transpose = self._transpose()
+ centers = transpose((x + width / 2, y + height / 2))
+ if nested:
+ error_coords = (self.view.y(nested.min), self.view.y(nested.max))
+ self._draw_error_marks(errors_node, x, error_coords, width, index)
+ return centers
def bar(self, serie_node, serie, index, rescale=False):
"""Draw a bar graph for a serie"""
bars = self.svg.node(serie_node['plot'], class_="bars")
+ errors_node = self.svg.node(serie_node['plot'], class_="errors_marks")
if rescale and self.secondary_series:
points = [
(x, self._scale_diff + (y - self._scale_min_2nd) * self._scale)
@@ -76,10 +84,12 @@ class Bar(Graph):
self.svg,
self.svg.node(bars, class_='bar'),
metadata)
- val = self._format(serie.values[i])
-
+ val = self._get_value(points, i)
+ nested = serie.values[i] if isinstance(serie.values[i],
+ NestedSerie) else None
x_center, y_center = self._bar(
- bar, x, y, index, i, self.zero, secondary=rescale)
+ bar, x, y, index, i, self.zero, errors_node,
+ secondary=rescale, nested=nested)
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/base.py b/pygal/graph/base.py
index bf77066..778d1a4 100644
--- a/pygal/graph/base.py
+++ b/pygal/graph/base.py
@@ -1,4 +1,4 @@
- # -*- coding: utf-8 -*-
+# -*- coding: utf-8 -*-
# This file is part of pygal
#
# A python svg graph plotting library
@@ -24,7 +24,8 @@ Base for pygal charts
from __future__ import division
from pygal.view import Margin, Box
from pygal.util import (
- get_text_box, get_texts_box, cut, rad, humanize, truncate, split_title)
+ get_text_box, get_texts_box, cut, rad, humanize, truncate,
+ split_title)
from pygal.svg import Svg
from pygal.util import cached_property
from math import sin, cos, sqrt
@@ -203,25 +204,29 @@ class BaseGraph(object):
def _secondary_min(self):
"""Getter for the minimum series value"""
return (self.range[0] if (self.range and self.range[0] is not None)
- else (min(self._secondary_values) if self._secondary_values else None))
+ else (min(serie.min for serie in self.secondary_series)
+ if self._secondary_values else None))
@cached_property
def _min(self):
"""Getter for the minimum series value"""
- return (self.range[0] if (self.range and self.range[0] is not None)
- else (min(self._values) if self._values else None))
+ return (self.range[0] if (self.range and self.range[0] is not None)
+ else (min(serie.min for serie in self.series)
+ if self._values else None))
@cached_property
def _max(self):
"""Getter for the maximum series value"""
return (self.range[1] if (self.range and self.range[1] is not None)
- else (max(self._values) if self._values else None))
+ else (max(serie.max for serie in self.series)
+ if self._values else None))
@cached_property
def _secondary_max(self):
"""Getter for the maximum series value"""
return (self.range[1] if (self.range and self.range[1] is not None)
- else (max(self._secondary_values) if self._secondary_values else None))
+ else (max(serie.max for serie in self.secondary_series)
+ if self._secondary_values else None))
@cached_property
def _order(self):
@@ -245,6 +250,7 @@ class BaseGraph(object):
return sum(
map(len, map(lambda s: s.safe_values, self.series))) != 0 and (
sum(map(abs, self._values)) != 0)
+ return any(map(lambda s: s.has_data, self.series))
def render(self, is_unicode=False):
"""Render the graph, and return the svg string"""
diff --git a/pygal/graph/datey.py b/pygal/graph/datey.py
index bda6d6e..f218f00 100644
--- a/pygal/graph/datey.py
+++ b/pygal/graph/datey.py
@@ -40,6 +40,7 @@ from pygal._compat import total_seconds
from pygal.adapters import date
from pygal.util import compute_scale
from pygal.graph.xy import XY
+from pygal.serie import NestedSerie
import datetime
@@ -67,21 +68,25 @@ class DateY(XY):
# Approximatively the same code as in XY.
# The only difference is the transformation of dates to numbers
# (beginning) and the reversed transformation to dates (end)
- self._offset = min([val[0]
- for serie in self.series
- for val in serie.values
- if val[0] is not None]
- or [datetime.datetime.fromtimestamp(0)])
+ self._offset = min([s.min for s in self.series]
+ or [datetime.datetime.fromtimestamp(0)])
+ # self._offset = min(
+ # [val[0].min if isinstance(val[0], NestedSerie)
+ # else val[0]
+ # for serie in self.series
+ # for val in serie._values
+ # if val[0] is not None]
+ # or [datetime.datetime.fromtimestamp(0)])
for serie in self.all_series:
- serie.values = [(self._tonumber(v[0]), v[1]) for v in serie.values]
+ serie._values = [(self._tonumber(v[0]), v[1]) for v in serie._values]
xvals = [val[0]
for serie in self.series
- for val in serie.values
+ for val in serie._values
if val[0] is not None]
yvals = [val[1]
for serie in self.series
- for val in serie.values
+ for val in serie._values
if val[1] is not None]
if xvals:
xmin = min(xvals)
diff --git a/pygal/graph/frenchmap.py b/pygal/graph/frenchmap.py
index 951c86d..f9202db 100644
--- a/pygal/graph/frenchmap.py
+++ b/pygal/graph/frenchmap.py
@@ -198,7 +198,7 @@ class FrenchMapDepartments(Graph):
"""Getter for series values (flattened)"""
return [val[1]
for serie in self.series
- for val in serie.values
+ for val in serie._values
if val[1] is not None]
def _plot(self):
@@ -208,12 +208,12 @@ class FrenchMapDepartments(Graph):
for i, serie in enumerate(self.series):
safe_vals = list(filter(
- lambda x: x is not None, cut(serie.values, 1)))
+ lambda x: x is not None, cut(serie._values, 1)))
if not safe_vals:
continue
min_ = min(safe_vals)
max_ = max(safe_vals)
- for j, (area_code, value) in enumerate(serie.values):
+ for j, (area_code, value) in enumerate(serie._values):
if isinstance(area_code, Number):
area_code = '%2d' % area_code
if value is None:
diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py
index 3e025cc..8737915 100644
--- a/pygal/graph/graph.py
+++ b/pygal/graph/graph.py
@@ -24,9 +24,11 @@ Commmon graphing functions
from __future__ import division
from pygal.interpolate import INTERPOLATIONS
from pygal.graph.base import BaseGraph
+from pygal.serie import NestedSerie
from pygal.view import View, LogView, XYLogView
from pygal.util import (
- majorize, truncate, reverse_text_len, get_texts_box, cut, rad, decorate)
+ majorize, truncate, reverse_text_len, get_texts_box, cut, rad, decorate,
+ swap, ident)
from math import sqrt, ceil, cos
from itertools import repeat, chain
@@ -493,12 +495,13 @@ class Graph(BaseGraph):
def _get_value(self, values, i):
"""Get the value formatted for tooltip"""
- return self._format(values[i][1])
+ return self._format(values[i][1].mean if isinstance(
+ values[i][1], NestedSerie) else values[i][1])
def _points(self, x_pos):
for serie in self.all_series:
serie.points = [
- (x_pos[i], v)
+ (x_pos[i], v.mean if isinstance(v, NestedSerie) else v)
for i, v in enumerate(serie.values)]
if serie.points and self.interpolate:
serie.interpolated = self._interpolate(x_pos, serie.values)
@@ -528,3 +531,12 @@ class Graph(BaseGraph):
def _post_compute(self):
pass
+
+ def _transpose(self):
+ return swap if self.horizontal else ident
+
+ def _draw_error_marks(self, node, x, error_coords, width, index):
+ """Draw error marks using std deviation."""
+ error_node = node
+ x = x + width / 2
+ self.svg.draw_errors(error_node, self._transpose(), x, error_coords)
diff --git a/pygal/graph/histogram.py b/pygal/graph/histogram.py
index 16c2f00..91ebcbc 100644
--- a/pygal/graph/histogram.py
+++ b/pygal/graph/histogram.py
@@ -37,7 +37,7 @@ class Histogram(Graph):
"""Getter for secondary series values (flattened)"""
return [val[0]
for serie in self.series
- for val in serie.values
+ for val in serie._values
if val[0] is not None]
@cached_property
@@ -45,14 +45,14 @@ class Histogram(Graph):
"""Getter for secondary series values (flattened)"""
return [val[0]
for serie in self.secondary_series
- for val in serie.values
+ for val in serie._values
if val[0] is not None]
@cached_property
def xvals(self):
return [val
for serie in self.all_series
- for dval in serie.values
+ for dval in serie._values
for val in dval[1:3]
if val is not None]
@@ -60,7 +60,7 @@ class Histogram(Graph):
def yvals(self):
return [val[0]
for serie in self.series
- for val in serie.values
+ for val in serie._values
if val[0] is not None]
def _has_data(self):
@@ -92,7 +92,7 @@ class Histogram(Graph):
bars = self.svg.node(serie_node['plot'], class_="histbars")
points = serie.points
- for i, (y, x0, x1) in enumerate(points):
+ for i, (y, x0, x1) in enumerate(serie._values):
if None in (x0, x1, y) or (self.logarithmic and y <= 0):
continue
metadata = serie.metadata.get(i)
@@ -101,7 +101,7 @@ class Histogram(Graph):
self.svg,
self.svg.node(bars, class_='histbar'),
metadata)
- val = self._format(serie.values[i][0])
+ val = self._format(serie._values[i][0])
x_center, y_center = self._bar(
bar, x0, x1, y, index, i, self.zero, secondary=rescale)
diff --git a/pygal/graph/line.py b/pygal/graph/line.py
index bf34ef6..f5b5889 100644
--- a/pygal/graph/line.py
+++ b/pygal/graph/line.py
@@ -22,6 +22,7 @@ Line chart
"""
from __future__ import division
from pygal.graph.graph import Graph
+from pygal.serie import NestedSerie
from pygal.util import cached_property, compute_scale, decorate
@@ -121,6 +122,17 @@ class Line(Graph):
serie_node['plot'], view_values, close=self._self_close,
class_='line reactive' + (' nofill' if not self.fill else ''))
+ for i, (x, y) in enumerate(points):
+ x = self.view.x(x)
+ errors_node = self.svg.node(serie_node['overlay'],
+ class_="errors_marks")
+ nested = serie.values[i] if isinstance(serie.values[i],
+ NestedSerie) else None
+ if nested:
+ error_coords = (self.view.y(nested.min),
+ self.view.y(nested.max))
+ self._draw_error_marks(errors_node, x, error_coords, 0, i)
+
def _compute(self):
# X Labels
x_pos = [
diff --git a/pygal/graph/stackedbar.py b/pygal/graph/stackedbar.py
index 8bd129c..442faad 100644
--- a/pygal/graph/stackedbar.py
+++ b/pygal/graph/stackedbar.py
@@ -90,7 +90,8 @@ class StackedBar(Bar):
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, errors_node, shift=False,
+ secondary=False, nested=None):
if secondary:
cumulation = (self.secondary_negative_cumulation
if y < self.zero else
@@ -124,4 +125,7 @@ class StackedBar(Bar):
x=x, y=y, rx=r, ry=r, width=width, height=height,
class_='rect reactive tooltip-trigger')
transpose = swap if self.horizontal else ident
+ if nested:
+ error_coords = (self.view.y(nested.min), self.view.y(nested.max))
+ self._draw_error_marks(errors_node, x, error_coords, width, index)
return transpose((x + width / 2, y + height / 2))
diff --git a/pygal/graph/supranationalworldmap.py b/pygal/graph/supranationalworldmap.py
index 4cc9a16..5bcf92d 100644
--- a/pygal/graph/supranationalworldmap.py
+++ b/pygal/graph/supranationalworldmap.py
@@ -43,13 +43,13 @@ class SupranationalWorldmap(Worldmap):
for i, serie in enumerate(self.series):
safe_vals = list(filter(
- lambda x: x is not None, cut(serie.values, 1)))
+ lambda x: x is not None, cut(serie._values, 1)))
if not safe_vals:
continue
min_ = min(safe_vals)
max_ = max(safe_vals)
- serie.values = self.replace_supranationals(serie.values)
- for j, (country_code, value) in enumerate(serie.values):
+ serie.values = self.replace_supranationals(serie._values)
+ for j, (country_code, value) in enumerate(serie._values):
if value is None:
continue
if max_ == min_:
diff --git a/pygal/graph/verticalpyramid.py b/pygal/graph/verticalpyramid.py
index 7c52ac9..cbd8f51 100644
--- a/pygal/graph/verticalpyramid.py
+++ b/pygal/graph/verticalpyramid.py
@@ -81,8 +81,9 @@ class VerticalPyramid(StackedBar):
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, errors_node, shift=True,
+ secondary=False, nested=None):
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, nested)
diff --git a/pygal/graph/worldmap.py b/pygal/graph/worldmap.py
index 8e144d6..cc86eb3 100644
--- a/pygal/graph/worldmap.py
+++ b/pygal/graph/worldmap.py
@@ -44,7 +44,7 @@ class Worldmap(Graph):
def countries(self):
return [val[0]
for serie in self.all_series
- for val in serie.values
+ for val in serie._values
if val[0] is not None]
@cached_property
@@ -52,7 +52,7 @@ class Worldmap(Graph):
"""Getter for series values (flattened)"""
return [val[1]
for serie in self.series
- for val in serie.values
+ for val in serie._values
if val[1] is not None]
def _plot(self):
@@ -62,12 +62,12 @@ class Worldmap(Graph):
for i, serie in enumerate(self.series):
safe_vals = list(filter(
- lambda x: x is not None, cut(serie.values, 1)))
+ lambda x: x is not None, cut(serie._values, 1)))
if not safe_vals:
continue
min_ = min(safe_vals)
max_ = max(safe_vals)
- for j, (country_code, value) in enumerate(serie.values):
+ for j, (country_code, value) in enumerate(serie._values):
if value is None:
continue
if max_ == min_:
diff --git a/pygal/graph/xy.py b/pygal/graph/xy.py
index 24272ac..a6ea110 100644
--- a/pygal/graph/xy.py
+++ b/pygal/graph/xy.py
@@ -24,6 +24,7 @@ XY Line graph
from __future__ import division
from pygal.util import compute_scale, cached_property
from pygal.graph.line import Line
+from pygal.serie import NestedSerie
class XY(Line):
@@ -32,16 +33,16 @@ class XY(Line):
@cached_property
def xvals(self):
- return [val[0]
+ return [val[0].mean if isinstance(val[0], NestedSerie) else val[0]
for serie in self.all_series
- for val in serie.values
+ for val in serie._values
if val[0] is not None]
@cached_property
def yvals(self):
- return [val[1]
+ return [val[1].mean if isinstance(val[1], NestedSerie) else val[1]
for serie in self.series
- for val in serie.values
+ for val in serie._values
if val[1] is not None]
def _has_data(self):
diff --git a/pygal/serie.py b/pygal/serie.py
index d3636b5..6b45b05 100644
--- a/pygal/serie.py
+++ b/pygal/serie.py
@@ -20,20 +20,95 @@
Little helpers for series
"""
-from pygal.util import cached_property
+from pygal.util import cached_property, cut
+from math import fsum, sqrt
class Serie(object):
"""Serie containing title, values and the graph serie index"""
- def __init__(self, title, values, metadata=None):
+ def __init__(self, title, values, metadata=None, parent=None, dual=False):
self.title = title
- self.values = values
+ self._values = values
self.metadata = metadata or {}
+ self.parent = parent
+ self.dual = dual
+
+
+ @cached_property
+ def values(self):
+ if self.dual:
+ return cut(self._values)
+ return self._values
@cached_property
def safe_values(self):
return list(filter(lambda x: x is not None, self.values))
+ @cached_property
+ def min(self):
+ """Returns the lowest value of the serie."""
+ return min([val.min if isinstance(val, NestedSerie) else val
+ for val in self.values if val is not None] or [None])
+
+ @cached_property
+ def max(self):
+ """Returns the lowest value of the serie."""
+ return max([val.max if isinstance(val, NestedSerie) else val
+ for val in self.values if val is not None] or [None])
+
+ @cached_property
+ def length(self):
+ """Returns the serie size."""
+ return len(self.values)
+
+ @cached_property
+ def has_data(self):
+ """True if data is provided."""
+ datalen = len(self.safe_values)
+ total = 0
+ for v in self.safe_values:
+ if v:
+ if isinstance(v, NestedSerie):
+ total += v.abs
+ elif isinstance(v, tuple):
+ total += any([abs(v[0] or 0) != 0, abs(v[1] or 0) != 0])
+ else:
+ total += abs(v)
+ return datalen and total != 0
+
+
+class NestedSerie(Serie):
+ """Class that handles nested series."""
+ @cached_property
+ def mean(self):
+ """Returns the average on the serie (mean)."""
+ return fsum([v for v in self.values]) / self.length
+
+ @cached_property
+ def variance(self):
+ """Returns the variance for the serie."""
+ return 1/self.length * fsum((v-self.mean) ** 2 for v in self.values)
+
+ @cached_property
+ def deviation(self):
+ """Returns the deviation for the serie."""
+ return sqrt(self.variance)
+
+ @cached_property
+ def min(self):
+ """Returns the lowest value of the serie."""
+ return self.mean - self.deviation
+
+ @cached_property
+ def max(self):
+ """Returns the lowest value of the serie."""
+ return self.mean + self.deviation
+
+ @cached_property
+ def abs(self):
+ """Returns the absolute value of the serie."""
+ return abs(self.mean)
+
class Label(object):
"""A label with his position"""
diff --git a/pygal/svg.py b/pygal/svg.py
index 76956fe..f1f872e 100644
--- a/pygal/svg.py
+++ b/pygal/svg.py
@@ -235,3 +235,32 @@ class Svg(object):
if self.graph.disable_xml_declaration or is_unicode:
svg = svg.decode('utf-8')
return svg
+
+ def draw_errors(self, parent_node, transpose, x, y_coords):
+ """Draws the chart errors aka confidence level."""
+ width = (
+ self.graph.view.x(1) - self.graph.view.x(0)) / self.graph._len
+ series_margin = width * getattr(self.graph, '_series_margin', 1)
+ width -= 2 * series_margin
+ y_begin = y_coords[0]
+ y_end = y_coords[1]
+ line_edges = transpose((x, y_begin)), transpose((x, y_end))
+ line_feet = (transpose((x - width / 4, y_begin)),
+ transpose((x + width / 4, y_begin)))
+ line_hat = (transpose((x - width / 4, y_end)),
+ transpose((x + width / 4, y_end)))
+ self.line(
+ parent_node,
+ coords=[line_edges[0], line_edges[1]],
+ class_='errors'
+ )
+ self.line(
+ parent_node,
+ coords=[line_feet[0], line_feet[1]],
+ class_='errors'
+ )
+ self.line(
+ parent_node,
+ coords=[line_hat[0], line_hat[1]],
+ class_='errors'
+ )
diff --git a/pygal/util.py b/pygal/util.py
index a0608a9..c33308a 100644
--- a/pygal/util.py
+++ b/pygal/util.py
@@ -318,7 +318,7 @@ def safe_enumerate(iterable):
if v is not None:
yield i, v
-from pygal.serie import Serie
+from pygal.serie import Serie, NestedSerie
def prepare_values(raw, config, cls):
@@ -376,6 +376,12 @@ def prepare_values(raw, config, cls):
raw_value = dict(raw_value)
value = raw_value.pop('value', None)
metadata[index] = raw_value
+ elif is_list_like(raw_value) and not cls._dual:
+ value = NestedSerie(title, raw_value, metadata)
+ elif (is_list_like(raw_value) and cls._dual and
+ len(raw_value) > 1 and is_list_like(raw_value[1])):
+ value = (raw_value[0],
+ NestedSerie(title, raw_value[1], metadata))
else:
value = raw_value
@@ -399,7 +405,11 @@ def prepare_values(raw, config, cls):
else:
value = adapter(value)
values.append(value)
- series.append(Serie(title, values, metadata))
+ serie = Serie(title, values, metadata, dual=cls._dual)
+ for v in serie.values:
+ if isinstance(v, NestedSerie):
+ v.parent = serie
+ series.append(serie)
return series