From 3b42619c058c6c9c95b8a7c119f3d5031a79fc34 Mon Sep 17 00:00:00 2001 From: Jean-Marc Martins Date: Wed, 14 May 2014 15:26:02 +0200 Subject: [PATCH] Adds errors in charts --- pygal/graph/bar.py | 12 ++++++++++++ pygal/graph/line.py | 10 ++++++++-- pygal/svg.py | 32 +++++++++++++++++++++++++++++++- pygal/util.py | 33 +++++++++++++++++++++++++++++++-- 4 files changed, 82 insertions(+), 5 deletions(-) diff --git a/pygal/graph/bar.py b/pygal/graph/bar.py index 26abb15..cfe5ebc 100644 --- a/pygal/graph/bar.py +++ b/pygal/graph/bar.py @@ -82,6 +82,18 @@ class Bar(Graph): x_center, y_center = self._bar( bar, x, y, index, i, self.zero, secondary=rescale, rounded=serie.rounded_bars) + + errors = metadata.get('errors') if metadata else None + if errors: + error_node = self.svg.node(bar, class_='errors') + base_x = x_center + transpose = ident + if self.horizontal: + transpose = swap + base_x = y_center + self.svg.draw_errors(error_node, errors, base_x, + transpose=transpose) + 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 cc69bdd..814eddc 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -103,8 +103,9 @@ class Line(Graph): 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=serie.dots_size, - class_='dot reactive tooltip-trigger') + 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) self._static_value( @@ -112,6 +113,11 @@ class Line(Graph): x + self.value_font_size, y + self.value_font_size) + errors = metadata.get('errors') if metadata else None + if errors: + error_node = self.svg.node(dots, class_='errors') + self.svg.draw_errors(error_node, errors, x) + if serie.stroke: if self.interpolate: view_values = list(map(self.view, serie.interpolated)) diff --git a/pygal/svg.py b/pygal/svg.py index f09bbff..4d25b87 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -30,7 +30,7 @@ from datetime import date, datetime from numbers import Number from lxml import etree from math import cos, sin, pi -from pygal.util import template, coord_format, minify_css +from pygal.util import template, coord_format, minify_css, ident from pygal import __version__ @@ -240,3 +240,33 @@ class Svg(object): if self.graph.disable_xml_declaration or is_unicode: svg = svg.decode('utf-8') return svg + + def draw_errors(self, node, errors, base_x, transpose=None): + """Draws the confidence level for a given point.""" + if not transpose: + transpose = ident + width = (self.graph.view.x(1) - self.graph.view.x(0)) / self.graph._len + series_margin = width * getattr(self.graph, '_series_margin', 0) + width -= 2 * series_margin + offset = (width - width / 2) / 2 + + lower_tip = transpose((base_x, self.graph.view.y(errors.get('min')))) + upper_tip = transpose((base_x, self.graph.view.y(errors.get('max')))) + + lower_left_tip = transpose((base_x - offset, + self.graph.view.y(errors.get('min')))) + lower_right_tip = transpose((base_x + offset, + self.graph.view.y(errors.get('min')))) + + upper_left_tip = transpose((base_x - offset, + self.graph.view.y(errors.get('max')))) + upper_right_tip = transpose((base_x + offset, + self.graph.view.y(errors.get('max')))) + + # base line + self.line(node, coords=[lower_tip, upper_tip], class_='error') + # hat/foot + self.line(node, coords=[lower_left_tip, lower_right_tip], + class_='error') + self.line(node, coords=[upper_left_tip, upper_right_tip], + class_='error') diff --git a/pygal/util.py b/pygal/util.py index 4607cde..ab94b74 100644 --- a/pygal/util.py +++ b/pygal/util.py @@ -24,7 +24,7 @@ from __future__ import division from pygal._compat import to_str, u, is_list_like import re from decimal import Decimal -from math import floor, pi, log, log10, ceil +from math import floor, pi, log, log10, ceil, fsum, sqrt from itertools import cycle from functools import reduce from pygal.adapters import not_zero, positive @@ -228,7 +228,7 @@ def get_texts_box(texts, fs): def decorate(svg, node, metadata): - """Add metedata next to a node""" + """Add metadata next to a node""" if not metadata: return node xlink = metadata.get('xlink') @@ -376,6 +376,13 @@ def prepare_values(raw, config, cls): if isinstance(raw_value, dict): raw_value = dict(raw_value) value = raw_value.pop('value', None) + if not value: + stdd = raw_value.pop('stdd', None) + errors = raw_value.pop('errors', None) + if stdd and is_list_like(stdd): + value = mean(stdd) + if not errors: + raw_value.update(dict(errors=compute_errors(stdd))) metadata[index] = raw_value else: value = raw_value @@ -423,3 +430,25 @@ def split_title(title, width, title_fs): title_line = title_line[i:].strip() titles.append(title_line) return titles + + +def mean(values): + """Return the mean of a serie of values.""" + return fsum(values) / len(values) + + +def variance(values): + """Return the variance of a serie of values.""" + values_mean = mean(values) + return fsum((v-values_mean) ** 2 for v in values) / len(values) + + +def std_deviation(values): + """Returns the standard deviation of values.""" + return sqrt(variance(values)) + + +def compute_errors(values): + m = mean(values) + stdd = std_deviation(values) + return dict(mean=m, min=m-stdd, max=m+stdd)