From 7f4929f5af4504a67fc0b9e324ec2aa74d870491 Mon Sep 17 00:00:00 2001 From: Jeffrey Starr Date: Tue, 28 Jan 2014 22:05:47 -0700 Subject: [PATCH] Initial commit of Box Plot --- pygal/graph/__init__.py | 3 +- pygal/graph/box.py | 146 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 pygal/graph/box.py diff --git a/pygal/graph/__init__.py b/pygal/graph/__init__.py index d464591..b91e9b3 100644 --- a/pygal/graph/__init__.py +++ b/pygal/graph/__init__.py @@ -39,5 +39,6 @@ CHARTS_NAMES = [ 'DateY', 'Worldmap', 'SupranationalWorldmap', - 'Histogram' + 'Histogram', + 'Box' ] diff --git a/pygal/graph/box.py b/pygal/graph/box.py new file mode 100644 index 0000000..2f94c96 --- /dev/null +++ b/pygal/graph/box.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# This file is part of pygal +# +# A python svg graph plotting library +# Copyright © 2012-2013 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 + + +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) + + def _compute(self): + """ + Compute parameters necessary for later steps within the rendering process + """ + # Note: this code was copied from Bar graph + 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 map(float, self.y_labels) + + self._x_labels = self.x_labels and list(zip(self.x_labels, [ + (i + .5) / self._len for i in range(self._len)])) + 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. + q0, q1, q2, q3, q4 = self._box_points(serie.values) + 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(q2) + + x_center, y_center = self._draw_box(box, (q0, q1, q2, q3, q4), index) + self._tooltip_data(box, val, x_center, y_center, classes="centered") + #print(val) + #self._static_value(box, 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._len + #x, y = self.view((x, y)) + series_margin = width * self._series_margin + #x += series_margin + width -= 2 * series_margin + #height = self.view.y(y_zero) - y + left_edge = self.view.x(0) + width * box_index + + # draw lines for whiskers - bottom, median, and top + for whisker in (quartiles[0], quartiles[2], quartiles[4]): + self.svg.line(parent_node, + coords=[(left_edge, self.view.y(whisker)), (left_edge + width, self.view.y(whisker))], + attrib={'stroke-width': 3}) + + # 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, + attrib={'fill-opacity': 0.25}) + + return (left_edge + width / 2, self.view.height / 2) + + @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 + + Uses quartile definition from Mendenhall, W. and Sincich, T. L. Statistics for Engineering and the + Sciences, 4th ed. Prentice-Hall, 1995. + """ + n = len(values) + if not n: + return 0, 0, 0, 0, 0 + else: + s = sorted(values) # sort the copy in case the originals must stay in original order + if n % 2 == 0: # n is even + q2 = (values[n // 2] + values[n // 2 + 1]) / 2 + else: + q2 = values[(n + 1) // 2] + + q1 = values[int(round((n + 1) / 4))] + q3 = values[int(round((3 * n + 3) / 4))] + iqr = q3 - q1 + q0 = q1 - 1.5 * iqr + q4 = q3 + 1.5 * iqr + return q0, q1, q2, q3, q4