Repository: bokeh/bokeh

Issue:
BoxPlot 1st and 4th whiskers are just of equal length
![screen shot 2016-01-21 at 17 55 20](https://cloud.githubusercontent.com/assets/1043805/12485685/12e21b6c-c068-11e5-8a98-2596b9cdd0c3.png)

Here at the picture are example car prices which are skewed data. Seems that median 50%, 25% and 75% drawn correct, but 0% and 100% whiskers somehow of the same length and this is impossible because data are skewed and plus there're no price below zero in my sample data. The lower whisker must be way shorter.



Discussion:
From the https://en.wikipedia.org/wiki/Interquartile_range:

> The interquartile range is often used to find outliers in data. Outliers are observations that fall below Q1 - 1.5(IQR) or above Q3 + 1.5(IQR). In a boxplot, the highest and lowest occurring value within this limit are drawn as bar of the whiskers, and the outliers as individual points.

So whiskers start on first data point inside the 1.5 \* IQR range, everything outside the range are indeed outliers, not just:

```
        self.w0 = self.q1 - (1.5 * self.iqr)
        self.w1 = self.q3 + (1.5 * self.iqr)
```

[bokeh-boxplot-whiskers.patch.txt](https://github.com/bokeh/bokeh/files/99691/bokeh-boxplot-whiskers.patch.txt)

How it looks now:

![screen shot 2016-01-21 at 20 49 43](https://cloud.githubusercontent.com/assets/1043805/12490861/71b0187a-c080-11e5-8e36-8c1009071757.png)

ping @fpliger @rothnic - is @danaki's patch correct (this is not an area I'm confident in). If so @danaki, we'd love a PR if you feel confident doing that.

@birdsarah It should be correct according to https://en.wikipedia.org/wiki/Interquartile_range (that is quite more detailed then) https://en.wikipedia.org/wiki/Box_plot.
Best would be to have a PR opened with a test (maybe with the same example shown in the first reference?). @danaki, would you be confident with that doing it?

My example data contains about 18K records, but from bar plot examples http://bokeh.pydata.org/en/0.10.0/docs/user_guide/charts.html#box-plots seems that "autompg" also drawn incorrectly. I'll try with PR.


Resolving patch:
diff --git a/bokeh/charts/glyphs.py b/bokeh/charts/glyphs.py
--- a/bokeh/charts/glyphs.py
+++ b/bokeh/charts/glyphs.py
@@ -846,8 +846,15 @@ def calc_quartiles(self):
         self.q2 = self.q2_glyph.end
         self.q3 = self.q3_glyph.end
         self.iqr = self.q3 - self.q1
-        self.w0 = self.q1 - (1.5 * self.iqr)
-        self.w1 = self.q3 + (1.5 * self.iqr)
+
+        mx = Max()
+        mx.set_data(self.values)
+
+        mn = Min()
+        mn.set_data(self.values)
+
+        self.w0 = max(self.q1 - (1.5 * self.iqr), mn.value)
+        self.w1 = min(self.q3 + (1.5 * self.iqr), mx.value)
 
     def build_source(self):
         """Calculate stats and builds and returns source for whiskers."""


Test patch:
diff --git a/bokeh/charts/tests/test_comp_glyphs.py b/bokeh/charts/tests/test_comp_glyphs.py
--- a/bokeh/charts/tests/test_comp_glyphs.py
+++ b/bokeh/charts/tests/test_comp_glyphs.py
@@ -2,7 +2,7 @@
 import pandas as pd
 from bokeh.charts.models import CompositeGlyph
 from bokeh.charts.glyphs import (AreaGlyph, LineGlyph, PointGlyph, StepGlyph,
-                                 BarGlyph)
+                                 BarGlyph, BoxGlyph)
 from bokeh.charts.operations import stack
 
 from bokeh.models import ColumnDataSource
@@ -91,4 +91,19 @@ def test_area_stacking():
     area2_stacked_values = [0, 3, 10, 3, 6, 11, 0, 0, 10, 5, 2, 9, 2, 0]
     comparison = pd.Series(area2_stacked_values) - pd.Series(area2.df[
                                                                  'y_values'].values[0])
-    assert comparison.sum() == 0
\ No newline at end of file
+    assert comparison.sum() == 0
+
+
+def test_boxplot():
+    # test source: https://en.wikipedia.org/wiki/Interquartile_range
+    data=[102, 104, 105, 107, 108, 109, 110, 112, 115, 116, 118]
+    box = BoxGlyph(label={'cat': 'a'}, values=data, color='red')
+
+    assert box.q1 == 106
+    assert box.q2 == 109
+    assert box.q3 == 113.5
+    assert box.iqr == 7.5
+
+    # test Interquartile range do not exceed data limits 
+    assert box.w0 == 102
+    assert box.w1 == 118