Browse Source

Fix box plot

pull/98/head
Florian Mounier 11 years ago
parent
commit
6630f548f3
  1. 2
      demo/moulinrouge/__init__.py
  2. 13
      demo/moulinrouge/tests.py
  3. 87
      pygal/graph/box.py
  4. 2
      pygal/graph/stackedbar.py

2
demo/moulinrouge/__init__.py

@ -31,7 +31,7 @@ import pickle
def random_label(): def random_label():
chars = string.letters + string.digits + u' àéèçêâäëï' chars = string.ascii_letters + string.digits + u' àéèçêâäëï'
return ''.join( return ''.join(
[random.choice(chars) [random.choice(chars)
for i in range(random.randrange(4, 30))]) for i in range(random.randrange(4, 30))])

13
demo/moulinrouge/tests.py

@ -2,7 +2,7 @@
# This file is part of pygal # This file is part of pygal
from pygal import ( from pygal import (
Bar, Gauge, Pyramid, Funnel, Dot, StackedBar, XY, 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 from pygal.style import styles
@ -277,6 +277,17 @@ def get_test_routes(app):
chart.add(10 * '2', [(8, 23), (21, 1), (5, 0)]) chart.add(10 * '2', [(8, 23), (21, 1), (5, 0)])
return chart.render_response() 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') @app.route('/test/stacked')
def test_stacked(): def test_stacked():
stacked = StackedBar() stacked = StackedBar()

87
pygal/graph/box.py

@ -23,13 +23,14 @@ Box plot
from __future__ import division from __future__ import division
from pygal.graph.graph import Graph from pygal.graph.graph import Graph
from pygal.util import compute_scale, decorate from pygal.util import compute_scale, decorate
from math import floor
# TODO: Implement tooltip
class Box(Graph): class Box(Graph):
""" """
Box plot 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. 1.5 times the interquartile range of the 25th and 75th percentiles.
See http://en.wikipedia.org/wiki/Box_plot See http://en.wikipedia.org/wiki/Box_plot
@ -41,9 +42,12 @@ class Box(Graph):
def _compute(self): 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: if self._min:
self._box.ymin = min(self._min, self.zero) self._box.ymin = min(self._min, self.zero)
if self._max: if self._max:
@ -60,7 +64,7 @@ class Box(Graph):
) if not self.y_labels else map(float, self.y_labels) ) if not self.y_labels else map(float, self.y_labels)
self._x_labels = self.x_labels and list(zip(self.x_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)) self._y_labels = list(zip(map(self._format, y_pos), y_pos))
def _plot(self): def _plot(self):
@ -74,9 +78,10 @@ class Box(Graph):
""" """
For a specific series, draw the box plot. 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 # Note: q0 and q4 do not literally mean the zero-th quartile
# the distance from 1.5 times the inter-quartile range to Q1 and Q3, respectively. # and the fourth quartile, but rather the distance from 1.5 times
q0, q1, q2, q3, q4 = self._box_points(serie.values) # 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") boxes = self.svg.node(serie_node['plot'], class_="boxes")
metadata = serie.metadata.get(0) 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) 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") 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): 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 width = (self.view.x(1) - self.view.x(0)) / self._order
#x, y = self.view((x, y))
series_margin = width * self._series_margin series_margin = width * self._series_margin
#x += series_margin left_edge = self.view.x(0) + width * box_index + series_margin
width -= 2 * 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 # draw lines for whiskers - bottom, median, and top
for whisker in (quartiles[0], quartiles[2], quartiles[4]): for i, whisker in enumerate(
self.svg.line(parent_node, (quartiles[0], quartiles[2], quartiles[4])):
coords=[(left_edge, self.view.y(whisker)), (left_edge + width, self.view.y(whisker))], whisker_width = width if i == 1 else width / 2
attrib={'stroke-width': 3}) 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) # draw lines connecting whiskers to box (Q1 and Q3)
self.svg.line(parent_node, self.svg.line(
coords=[(left_edge + width / 2, self.view.y(quartiles[0])), parent_node,
(left_edge + width / 2, self.view.y(quartiles[1]))], coords=[(left_edge + width / 2, self.view.y(quartiles[0])),
attrib={'stroke-width': 2}) (left_edge + width / 2, self.view.y(quartiles[1]))],
self.svg.line(parent_node, attrib={'stroke-width': 2})
coords=[(left_edge + width / 2, self.view.y(quartiles[4])), self.svg.line(
(left_edge + width / 2, self.view.y(quartiles[3]))], parent_node,
attrib={'stroke-width': 2}) 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 # box, bounded by Q1 and Q3
self.svg.node(parent_node, self.svg.node(
tag='rect', parent_node,
x=left_edge, tag='rect',
y=self.view.y(quartiles[1]), x=left_edge,
height=self.view.y(quartiles[3]) - self.view.y(quartiles[1]), y=self.view.y(quartiles[1]),
width=width, height=self.view.y(quartiles[3]) - self.view.y(quartiles[1]),
attrib={'fill-opacity': 0.25}) width=width,
attrib={'fill-opacity': 0.25})
return (left_edge + width / 2, self.view.height / 2) return (left_edge + width / 2, self.view.height / 2)
@staticmethod @staticmethod
def _box_points(values): 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. 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. Sciences, 4th ed. Prentice-Hall, 1995.
""" """
def median(seq): def median(seq):

2
pygal/graph/stackedbar.py

@ -34,7 +34,7 @@ class StackedBar(Bar):
def _get_separated_values(self, secondary=False): def _get_separated_values(self, secondary=False):
series = self.secondary_series if secondary else self.series 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([ positive_vals = [sum([
val for val in vals val for val in vals
if val is not None and val >= self.zero]) if val is not None and val >= self.zero])

Loading…
Cancel
Save