diff --git a/demo/moulinrouge/__init__.py b/demo/moulinrouge/__init__.py index 8b78a43..6b2d917 100644 --- a/demo/moulinrouge/__init__.py +++ b/demo/moulinrouge/__init__.py @@ -31,7 +31,7 @@ import pickle def random_label(): - chars = string.letters + string.digits + u' àéèçêâäëï' + chars = string.ascii_letters + string.digits + u' àéèçêâäëï' return ''.join( [random.choice(chars) for i in range(random.randrange(4, 30))]) diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index 8fa22d3..50fa9bf 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -2,7 +2,7 @@ # This file is part of pygal from pygal import ( Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, XY, - CHARTS_BY_NAME, Config, Line, DateY, Worldmap, Histogram) + CHARTS_BY_NAME, Config, Line, DateY, Worldmap, Histogram, Box) from pygal.style import styles @@ -277,6 +277,17 @@ def get_test_routes(app): chart.add(10 * '2', [(8, 23), (21, 1), (5, 0)]) return chart.render_response() + @app.route('/test/box') + def test_box(): + chart = Box() + chart.add('One', [15, 8, 2, -12, 9, 23]) + chart.add('Two', [5, 8, 2, -9, 23, 12]) + chart.add('Three', [8, -2, 12, -5, 9, 3]) + chart.add('Four', [5, 8, 2, -9, -3, 12]) + chart.add('Five', [8, 12, 12, -9, 5, 13]) + chart.x_labels = map(str, range(5)) + return chart.render_response() + @app.route('/test/stacked') def test_stacked(): stacked = StackedBar() diff --git a/pygal/graph/box.py b/pygal/graph/box.py index 04d696c..bffd748 100644 --- a/pygal/graph/box.py +++ b/pygal/graph/box.py @@ -23,13 +23,14 @@ Box plot from __future__ import division from pygal.graph.graph import Graph from pygal.util import compute_scale, decorate -from math import floor +# TODO: Implement tooltip class Box(Graph): """ Box plot - For each series, shows the median value, the 25th and 75th percentiles, and the values within + 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 @@ -41,9 +42,12 @@ class Box(Graph): def _compute(self): """ - Compute parameters necessary for later steps within the rendering process + Compute parameters necessary for later steps + within the rendering process """ - # Note: this code was copied from Bar graph + 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: @@ -60,7 +64,7 @@ class Box(Graph): ) 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)])) + (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): @@ -74,9 +78,10 @@ class Box(Graph): """ 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) + # 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 = serie.values boxes = self.svg.node(serie_node['plot'], class_="boxes") metadata = serie.metadata.get(0) @@ -89,56 +94,64 @@ class Box(Graph): 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. + 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)) + width = (self.view.x(1) - self.view.x(0)) / self._order series_margin = width * self._series_margin - #x += series_margin + left_edge = self.view.x(0) + width * box_index + 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}) + 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))], + 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]))], - 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]))], - attrib={'stroke-width': 2}) + self.svg.line( + parent_node, + coords=[(left_edge + width / 2, self.view.y(quartiles[0])), + (left_edge + width / 2, self.view.y(quartiles[1]))], + 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]))], + 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, - attrib={'fill-opacity': 0.25}) + 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. + 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 + Uses quartile definition from Mendenhall, W. and + Sincich, T. L. Statistics for Engineering and the Sciences, 4th ed. Prentice-Hall, 1995. """ def median(seq): diff --git a/pygal/graph/stackedbar.py b/pygal/graph/stackedbar.py index 610eacb..53423fa 100644 --- a/pygal/graph/stackedbar.py +++ b/pygal/graph/stackedbar.py @@ -34,7 +34,7 @@ class StackedBar(Bar): def _get_separated_values(self, secondary=False): series = self.secondary_series if secondary else self.series - transposed = zip(*[serie.values for serie in series]) + transposed = list(zip(*[serie.values for serie in series])) positive_vals = [sum([ val for val in vals if val is not None and val >= self.zero])