diff --git a/pygal/graph/box.py b/pygal/graph/box.py
index 2f94c96..ca9f672 100644
--- a/pygal/graph/box.py
+++ b/pygal/graph/box.py
@@ -23,6 +23,7 @@ Box plot
from __future__ import division
from pygal.graph.graph import Graph
from pygal.util import compute_scale, decorate
+from math import floor
class Box(Graph):
@@ -123,23 +124,44 @@ class Box(Graph):
@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
Sciences, 4th ed. Prentice-Hall, 1995.
"""
- n = len(values)
+ def median(seq):
+ n = len(seq)
+ if n % 2 == 0: # seq has an even length
+ return (seq[n // 2] + s[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:
- 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))]
+ 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
diff --git a/pygal/test/test_box.py b/pygal/test/test_box.py
new file mode 100644
index 0000000..d664827
--- /dev/null
+++ b/pygal/test/test_box.py
@@ -0,0 +1,62 @@
+# -*- 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 .
+from pygal.graph.box import Box
+from pygal import Box as ghostedBox
+
+
+def test_quartiles():
+ a = [-2.0, 3.0, 4.0, 5.0, 8.0] # odd test data
+ q0, q1, q2, q3, q4 = Box._box_points(a)
+
+ assert q1 == 7.0 / 4.0
+ assert q2 == 4.0
+ assert q3 == 23 / 4.0
+ assert q0 == 7.0 / 4.0 - 6.0 # q1 - 1.5 * iqr
+ assert q4 == 23 / 4.0 + 6.0 # q3 + 1.5 * iqr
+
+ b = [1.0, 4.0, 6.0, 8.0] # even test data
+ q0, q1, q2, q3, q4 = Box._box_points(b)
+
+ assert q2 == 5.0
+
+ c = [2.0, None, 4.0, 6.0, None] # odd with None elements
+ q0, q1, q2, q3, q4 = Box._box_points(c)
+
+ assert q2 == 4.0
+
+ d = [4]
+ q0, q1, q2, q3, q4 = Box._box_points(d)
+
+ assert q0 == 4
+ assert q1 == 4
+ assert q2 == 4
+ assert q3 == 4
+ assert q4 == 4
+
+
+def test_simple_box():
+ box = ghostedBox()
+ box.add('test1', [-1, 2, 3, 3.1, 3.2, 4, 5])
+ box.add('test2', [2, 3, 5, 6, 6, 4])
+ box.title = 'Box test'
+ q = box.render_pyquery()
+
+ assert len(q(".axis.y")) == 1
+ assert len(q(".legend")) == 2
+ assert len(q(".plot .series rect")) == 2
diff --git a/pygal/test/test_config.py b/pygal/test/test_config.py
index d828cb8..ee992c6 100644
--- a/pygal/test/test_config.py
+++ b/pygal/test/test_config.py
@@ -18,7 +18,7 @@
# along with pygal. If not, see .
from pygal import (
Line, Dot, Pie, Radar, Config, Bar, Funnel, Worldmap,
- SupranationalWorldmap, Histogram, Gauge)
+ SupranationalWorldmap, Histogram, Gauge, Box)
from pygal._compat import u
from pygal.test.utils import texts
from pygal.test import pytest_generate_tests, make_data
@@ -270,7 +270,7 @@ def test_no_data():
def test_include_x_axis(Chart):
chart = Chart()
if Chart in (Pie, Radar, Funnel, Dot, Gauge, Worldmap,
- SupranationalWorldmap, Histogram):
+ SupranationalWorldmap, Histogram, Box):
return
if not chart.cls._dual:
data = 100, 200, 150
diff --git a/pygal/test/test_graph.py b/pygal/test/test_graph.py
index fd711c0..eab95a9 100644
--- a/pygal/test/test_graph.py
+++ b/pygal/test/test_graph.py
@@ -68,7 +68,9 @@ def test_render_to_png(Chart, datas):
def test_metadata(Chart):
chart = Chart()
v = range(7)
- if Chart == pygal.XY:
+ if Chart in (pygal.Box,):
+ return # summary charts cannot display per-value metadata
+ elif Chart == pygal.XY:
v = list(map(lambda x: (x, x + 1), v))
elif Chart == pygal.Worldmap or Chart == pygal.SupranationalWorldmap:
v = list(map(lambda x: x, i18n.COUNTRIES))