Browse Source

Broken errors are broken

113_error_bars
Jean-Marc Martins 11 years ago
parent
commit
d11aa40600
  1. 2
      pygal/config.py
  2. 4
      pygal/css/style.css
  3. 208
      pygal/graph/#box.py#
  4. 87
      pygal/graph/#pie.py#
  5. 24
      pygal/graph/bar.py
  6. 18
      pygal/graph/base.py
  7. 21
      pygal/graph/datey.py
  8. 6
      pygal/graph/frenchmap.py
  9. 18
      pygal/graph/graph.py
  10. 12
      pygal/graph/histogram.py
  11. 12
      pygal/graph/line.py
  12. 6
      pygal/graph/stackedbar.py
  13. 6
      pygal/graph/supranationalworldmap.py
  14. 5
      pygal/graph/verticalpyramid.py
  15. 8
      pygal/graph/worldmap.py
  16. 9
      pygal/graph/xy.py
  17. 81
      pygal/serie.py
  18. 29
      pygal/svg.py
  19. 14
      pygal/util.py

2
pygal/config.py

@ -126,6 +126,8 @@ class Config(MetaConfig('ConfigBase', (object,), {})):
None, str, "Look", None, str, "Look",
"Graph Y-Axis title.", "Leave it to None to disable Y-Axis title.") "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( width = Key(
800, int, "Look", "Graph width") 800, int, "Look", "Graph width")

4
pygal/css/style.css

@ -129,6 +129,10 @@
stroke-width: 10; stroke-width: 10;
} }
{{ id }}.err_marks .errors {
stroke: {{ style.foreground_dark }};
}
{{ colors }} {{ colors }}

208
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 <http://www.gnu.org/licenses/>.
"""
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

87
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 <http://www.gnu.org/licenses/>.
"""
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

24
pygal/graph/bar.py

@ -23,7 +23,8 @@ Bar chart
from __future__ import division from __future__ import division
from pygal.graph.graph import Graph 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): class Bar(Graph):
@ -36,8 +37,10 @@ class Bar(Graph):
self._x_ranges = None self._x_ranges = None
super(Bar, self).__init__(*args, **kwargs) 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 width = (self.view.x(1) - self.view.x(0)) / self._len
x, y = self.view((x, y)) x, y = self.view((x, y))
series_margin = width * self._series_margin series_margin = width * self._series_margin
x += series_margin x += series_margin
@ -54,12 +57,17 @@ class Bar(Graph):
parent, 'rect', parent, 'rect',
x=x, y=y, rx=r, ry=r, width=width, height=height, x=x, y=y, rx=r, ry=r, width=width, height=height,
class_='rect reactive tooltip-trigger') class_='rect reactive tooltip-trigger')
transpose = swap if self.horizontal else ident transpose = self._transpose()
return transpose((x + width / 2, y + height / 2)) 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): def bar(self, serie_node, serie, index, rescale=False):
"""Draw a bar graph for a serie""" """Draw a bar graph for a serie"""
bars = self.svg.node(serie_node['plot'], class_="bars") 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: if rescale and self.secondary_series:
points = [ points = [
(x, self._scale_diff + (y - self._scale_min_2nd) * self._scale) (x, self._scale_diff + (y - self._scale_min_2nd) * self._scale)
@ -76,10 +84,12 @@ class Bar(Graph):
self.svg, self.svg,
self.svg.node(bars, class_='bar'), self.svg.node(bars, class_='bar'),
metadata) 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( 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( self._tooltip_data(
bar, val, x_center, y_center, classes="centered") bar, val, x_center, y_center, classes="centered")
self._static_value(serie_node, val, x_center, y_center) self._static_value(serie_node, val, x_center, y_center)

18
pygal/graph/base.py

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# This file is part of pygal # This file is part of pygal
# #
# A python svg graph plotting library # A python svg graph plotting library
@ -24,7 +24,8 @@ Base for pygal charts
from __future__ import division from __future__ import division
from pygal.view import Margin, Box from pygal.view import Margin, Box
from pygal.util import ( 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.svg import Svg
from pygal.util import cached_property from pygal.util import cached_property
from math import sin, cos, sqrt from math import sin, cos, sqrt
@ -203,25 +204,29 @@ class BaseGraph(object):
def _secondary_min(self): def _secondary_min(self):
"""Getter for the minimum series value""" """Getter for the minimum series value"""
return (self.range[0] if (self.range and self.range[0] is not None) 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 @cached_property
def _min(self): def _min(self):
"""Getter for the minimum series value""" """Getter for the minimum series value"""
return (self.range[0] if (self.range and self.range[0] is not None) return (self.range[0] if (self.range and self.range[0] is not None)
else (min(self._values) if self._values else None)) else (min(serie.min for serie in self.series)
if self._values else None))
@cached_property @cached_property
def _max(self): def _max(self):
"""Getter for the maximum series value""" """Getter for the maximum series value"""
return (self.range[1] if (self.range and self.range[1] is not None) 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 @cached_property
def _secondary_max(self): def _secondary_max(self):
"""Getter for the maximum series value""" """Getter for the maximum series value"""
return (self.range[1] if (self.range and self.range[1] is not None) 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 @cached_property
def _order(self): def _order(self):
@ -245,6 +250,7 @@ class BaseGraph(object):
return sum( return sum(
map(len, map(lambda s: s.safe_values, self.series))) != 0 and ( map(len, map(lambda s: s.safe_values, self.series))) != 0 and (
sum(map(abs, self._values)) != 0) sum(map(abs, self._values)) != 0)
return any(map(lambda s: s.has_data, self.series))
def render(self, is_unicode=False): def render(self, is_unicode=False):
"""Render the graph, and return the svg string""" """Render the graph, and return the svg string"""

21
pygal/graph/datey.py

@ -40,6 +40,7 @@ from pygal._compat import total_seconds
from pygal.adapters import date from pygal.adapters import date
from pygal.util import compute_scale from pygal.util import compute_scale
from pygal.graph.xy import XY from pygal.graph.xy import XY
from pygal.serie import NestedSerie
import datetime import datetime
@ -67,21 +68,25 @@ class DateY(XY):
# Approximatively the same code as in XY. # Approximatively the same code as in XY.
# The only difference is the transformation of dates to numbers # The only difference is the transformation of dates to numbers
# (beginning) and the reversed transformation to dates (end) # (beginning) and the reversed transformation to dates (end)
self._offset = min([val[0] self._offset = min([s.min for s in self.series]
for serie in self.series or [datetime.datetime.fromtimestamp(0)])
for val in serie.values # self._offset = min(
if val[0] is not None] # [val[0].min if isinstance(val[0], NestedSerie)
or [datetime.datetime.fromtimestamp(0)]) # 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: 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] xvals = [val[0]
for serie in self.series for serie in self.series
for val in serie.values for val in serie._values
if val[0] is not None] if val[0] is not None]
yvals = [val[1] yvals = [val[1]
for serie in self.series for serie in self.series
for val in serie.values for val in serie._values
if val[1] is not None] if val[1] is not None]
if xvals: if xvals:
xmin = min(xvals) xmin = min(xvals)

6
pygal/graph/frenchmap.py

@ -198,7 +198,7 @@ class FrenchMapDepartments(Graph):
"""Getter for series values (flattened)""" """Getter for series values (flattened)"""
return [val[1] return [val[1]
for serie in self.series for serie in self.series
for val in serie.values for val in serie._values
if val[1] is not None] if val[1] is not None]
def _plot(self): def _plot(self):
@ -208,12 +208,12 @@ class FrenchMapDepartments(Graph):
for i, serie in enumerate(self.series): for i, serie in enumerate(self.series):
safe_vals = list(filter( 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: if not safe_vals:
continue continue
min_ = min(safe_vals) min_ = min(safe_vals)
max_ = max(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): if isinstance(area_code, Number):
area_code = '%2d' % area_code area_code = '%2d' % area_code
if value is None: if value is None:

18
pygal/graph/graph.py

@ -24,9 +24,11 @@ Commmon graphing functions
from __future__ import division from __future__ import division
from pygal.interpolate import INTERPOLATIONS from pygal.interpolate import INTERPOLATIONS
from pygal.graph.base import BaseGraph from pygal.graph.base import BaseGraph
from pygal.serie import NestedSerie
from pygal.view import View, LogView, XYLogView from pygal.view import View, LogView, XYLogView
from pygal.util import ( 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 math import sqrt, ceil, cos
from itertools import repeat, chain from itertools import repeat, chain
@ -493,12 +495,13 @@ class Graph(BaseGraph):
def _get_value(self, values, i): def _get_value(self, values, i):
"""Get the value formatted for tooltip""" """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): def _points(self, x_pos):
for serie in self.all_series: for serie in self.all_series:
serie.points = [ serie.points = [
(x_pos[i], v) (x_pos[i], v.mean if isinstance(v, NestedSerie) else v)
for i, v in enumerate(serie.values)] for i, v in enumerate(serie.values)]
if serie.points and self.interpolate: if serie.points and self.interpolate:
serie.interpolated = self._interpolate(x_pos, serie.values) serie.interpolated = self._interpolate(x_pos, serie.values)
@ -528,3 +531,12 @@ class Graph(BaseGraph):
def _post_compute(self): def _post_compute(self):
pass 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)

12
pygal/graph/histogram.py

@ -37,7 +37,7 @@ class Histogram(Graph):
"""Getter for secondary series values (flattened)""" """Getter for secondary series values (flattened)"""
return [val[0] return [val[0]
for serie in self.series for serie in self.series
for val in serie.values for val in serie._values
if val[0] is not None] if val[0] is not None]
@cached_property @cached_property
@ -45,14 +45,14 @@ class Histogram(Graph):
"""Getter for secondary series values (flattened)""" """Getter for secondary series values (flattened)"""
return [val[0] return [val[0]
for serie in self.secondary_series for serie in self.secondary_series
for val in serie.values for val in serie._values
if val[0] is not None] if val[0] is not None]
@cached_property @cached_property
def xvals(self): def xvals(self):
return [val return [val
for serie in self.all_series for serie in self.all_series
for dval in serie.values for dval in serie._values
for val in dval[1:3] for val in dval[1:3]
if val is not None] if val is not None]
@ -60,7 +60,7 @@ class Histogram(Graph):
def yvals(self): def yvals(self):
return [val[0] return [val[0]
for serie in self.series for serie in self.series
for val in serie.values for val in serie._values
if val[0] is not None] if val[0] is not None]
def _has_data(self): def _has_data(self):
@ -92,7 +92,7 @@ class Histogram(Graph):
bars = self.svg.node(serie_node['plot'], class_="histbars") bars = self.svg.node(serie_node['plot'], class_="histbars")
points = serie.points 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): if None in (x0, x1, y) or (self.logarithmic and y <= 0):
continue continue
metadata = serie.metadata.get(i) metadata = serie.metadata.get(i)
@ -101,7 +101,7 @@ class Histogram(Graph):
self.svg, self.svg,
self.svg.node(bars, class_='histbar'), self.svg.node(bars, class_='histbar'),
metadata) metadata)
val = self._format(serie.values[i][0]) val = self._format(serie._values[i][0])
x_center, y_center = self._bar( 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)

12
pygal/graph/line.py

@ -22,6 +22,7 @@ Line chart
""" """
from __future__ import division from __future__ import division
from pygal.graph.graph import Graph from pygal.graph.graph import Graph
from pygal.serie import NestedSerie
from pygal.util import cached_property, compute_scale, decorate 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, serie_node['plot'], view_values, close=self._self_close,
class_='line reactive' + (' nofill' if not self.fill else '')) 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): def _compute(self):
# X Labels # X Labels
x_pos = [ x_pos = [

6
pygal/graph/stackedbar.py

@ -90,7 +90,8 @@ class StackedBar(Bar):
self._secondary_max = (positive_vals and max( self._secondary_max = (positive_vals and max(
sum_(max(positive_vals)), self.zero)) or self.zero 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: if secondary:
cumulation = (self.secondary_negative_cumulation cumulation = (self.secondary_negative_cumulation
if y < self.zero else if y < self.zero else
@ -124,4 +125,7 @@ class StackedBar(Bar):
x=x, y=y, rx=r, ry=r, width=width, height=height, x=x, y=y, rx=r, ry=r, width=width, height=height,
class_='rect reactive tooltip-trigger') class_='rect reactive tooltip-trigger')
transpose = swap if self.horizontal else ident 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)) return transpose((x + width / 2, y + height / 2))

6
pygal/graph/supranationalworldmap.py

@ -43,13 +43,13 @@ class SupranationalWorldmap(Worldmap):
for i, serie in enumerate(self.series): for i, serie in enumerate(self.series):
safe_vals = list(filter( 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: if not safe_vals:
continue continue
min_ = min(safe_vals) min_ = min(safe_vals)
max_ = max(safe_vals) max_ = max(safe_vals)
serie.values = self.replace_supranationals(serie.values) serie.values = self.replace_supranationals(serie._values)
for j, (country_code, value) in enumerate(serie.values): for j, (country_code, value) in enumerate(serie._values):
if value is None: if value is None:
continue continue
if max_ == min_: if max_ == min_:

5
pygal/graph/verticalpyramid.py

@ -81,8 +81,9 @@ class VerticalPyramid(StackedBar):
y) y)
for y in y_pos] 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: if index % 2:
y = -y y = -y
return super(VerticalPyramid, self)._bar( return super(VerticalPyramid, self)._bar(
parent, x, y, index, i, zero, False, secondary) parent, x, y, index, i, zero, False, secondary, nested)

8
pygal/graph/worldmap.py

@ -44,7 +44,7 @@ class Worldmap(Graph):
def countries(self): def countries(self):
return [val[0] return [val[0]
for serie in self.all_series for serie in self.all_series
for val in serie.values for val in serie._values
if val[0] is not None] if val[0] is not None]
@cached_property @cached_property
@ -52,7 +52,7 @@ class Worldmap(Graph):
"""Getter for series values (flattened)""" """Getter for series values (flattened)"""
return [val[1] return [val[1]
for serie in self.series for serie in self.series
for val in serie.values for val in serie._values
if val[1] is not None] if val[1] is not None]
def _plot(self): def _plot(self):
@ -62,12 +62,12 @@ class Worldmap(Graph):
for i, serie in enumerate(self.series): for i, serie in enumerate(self.series):
safe_vals = list(filter( 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: if not safe_vals:
continue continue
min_ = min(safe_vals) min_ = min(safe_vals)
max_ = max(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: if value is None:
continue continue
if max_ == min_: if max_ == min_:

9
pygal/graph/xy.py

@ -24,6 +24,7 @@ XY Line graph
from __future__ import division from __future__ import division
from pygal.util import compute_scale, cached_property from pygal.util import compute_scale, cached_property
from pygal.graph.line import Line from pygal.graph.line import Line
from pygal.serie import NestedSerie
class XY(Line): class XY(Line):
@ -32,16 +33,16 @@ class XY(Line):
@cached_property @cached_property
def xvals(self): 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 serie in self.all_series
for val in serie.values for val in serie._values
if val[0] is not None] if val[0] is not None]
@cached_property @cached_property
def yvals(self): def yvals(self):
return [val[1] return [val[1].mean if isinstance(val[1], NestedSerie) else val[1]
for serie in self.series for serie in self.series
for val in serie.values for val in serie._values
if val[1] is not None] if val[1] is not None]
def _has_data(self): def _has_data(self):

81
pygal/serie.py

@ -20,20 +20,95 @@
Little helpers for series 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): class Serie(object):
"""Serie containing title, values and the graph serie index""" """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.title = title
self.values = values self._values = values
self.metadata = metadata or {} 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 @cached_property
def safe_values(self): def safe_values(self):
return list(filter(lambda x: x is not None, self.values)) 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): class Label(object):
"""A label with his position""" """A label with his position"""

29
pygal/svg.py

@ -235,3 +235,32 @@ class Svg(object):
if self.graph.disable_xml_declaration or is_unicode: if self.graph.disable_xml_declaration or is_unicode:
svg = svg.decode('utf-8') svg = svg.decode('utf-8')
return svg 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'
)

14
pygal/util.py

@ -318,7 +318,7 @@ def safe_enumerate(iterable):
if v is not None: if v is not None:
yield i, v yield i, v
from pygal.serie import Serie from pygal.serie import Serie, NestedSerie
def prepare_values(raw, config, cls): def prepare_values(raw, config, cls):
@ -376,6 +376,12 @@ def prepare_values(raw, config, cls):
raw_value = dict(raw_value) raw_value = dict(raw_value)
value = raw_value.pop('value', None) value = raw_value.pop('value', None)
metadata[index] = raw_value 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: else:
value = raw_value value = raw_value
@ -399,7 +405,11 @@ def prepare_values(raw, config, cls):
else: else:
value = adapter(value) value = adapter(value)
values.append(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 return series

Loading…
Cancel
Save