Repository: bokeh/bokeh

Issue:
Odd Histogram behaviour
Reported on mailing list about a month ago, but still persists:

```
import bokeh.charts as bc
import bokeh
from collections import OrderedDict
#import pandas as pd

bokeh.load_notebook()
somelist = [100,100,200,200,300,300,400,400,400,400,500,600,600,600,700]

num_content = OrderedDict(billy=somelist)
hist = bc.Histogram(num_content, bins=2, notebook=True)
hist.title("some_test").width(400).height(350).legend(True).show()
```

`y-axis` values on the order of 10^-3



Discussion:
As I said in the mailing list, this is actually not a bug... it is the expression that we are using a `numpy.histogram` function under the hood and, with a `density=True` parameter, which gives you a "normalized" probability.
With `density=True`:

``` python
a = [100,100,200,200,300,300,400,400,400,400,500,600,600,600,700]
hist, bin_edges = np.histogram(a, bins=2, density=True)

hist
array([ 0.00133333,  0.002     ])
```

instead, with `density =False`:

``` python
hist, bin_edges = np.histogram(a, bins=2, density=False)

hist
array([6, 9])
```

We can probably expose the density parameter so the user can easily change the behaviour.

Thanks for the explanation damian and mentioning density=False.

@damianavila thanks for running this down. If `np.histogram` has just a few tunable parameters then we can probably expose them individually. If `np.histogram` has like twenty knobs and dials that people can turn, it might make sense to expose just one new paramaters, which is a dictionary of kwargs to pass on to `np.histogram`

I think we can expose just `density` for now (which is the most important) and later give them a stronger solution to tune up several parameters (in a more general solution encompassing charts tweaks). 

:+1: IIRC density is the only thing param we are not exposing on Histogram

great! should be simple solution


Resolving patch:
diff --git a/bokeh/charts/histogram.py b/bokeh/charts/histogram.py
--- a/bokeh/charts/histogram.py
+++ b/bokeh/charts/histogram.py
@@ -52,7 +52,7 @@ class Histogram(ChartObject):
         hist.title("Histogram").ylabel("frequency")
         hist.legend(True).width(400).height(350).show()
     """
-    def __init__(self, values, bins, mu=None, sigma=None,
+    def __init__(self, values, bins, mu=None, sigma=None, density=True,
                  title=None, xlabel=None, ylabel=None, legend=False,
                  xscale="linear", yscale="linear", width=800, height=600,
                  tools=True, filename=False, server=False, notebook=False,
@@ -63,45 +63,50 @@ def __init__(self, values, bins, mu=None, sigma=None,
                 values matrix.
             bins (int): number of bins to use in the Histogram building.
             mu (float, optional): theoretical mean value for the normal
-                distribution. Defaults to None.
+                distribution. (default: None)
             sigma (float, optional): theoretical sigma value for the
-                normal distribution. Defaults to None.
-            title (str, optional): the title of your chart. Defaults
-                to None.
+                normal distribution. (default: None)
+            density (bool, optional):  If False, the result will contain
+                the number of samples in each bin.  If True, the result
+                is the value of the probability *density* function at
+                the bin, normalized such that the *integral* over the
+                range is 1. For more info check numpy.histogram
+                function documentation. (default: True)
+            title (str, optional): the title of your chart.
+                (default: None)
             xlabel (str, optional): the x-axis label of your chart.
-                Defaults to None.
+                (default: None)
             ylabel (str, optional): the y-axis label of your chart.
-                Defaults to None.
+                (default: None)
             legend (str, optional): the legend of your chart. The legend
                 content is inferred from incoming input.It can be
                 ``top_left``, ``top_right``, ``bottom_left``,
                 ``bottom_right``. ``top_right`` is set if you set it
-                 as True. Defaults to None.
+                 as True. (default: False)
             xscale (str, optional): the x-axis type scale of your chart.
                 It can be ``linear``, ``datetime`` or ``categorical``.
-                Defaults to ``datetime``.
+                (default: ``linear``)
             yscale (str, optional): the y-axis type scale of your chart.
                 It can be ``linear``, ``datetime`` or ``categorical``.
-                Defaults to ``linear``.
+                (default: ``linear``)
             width (int, optional): the width of your chart in pixels.
-                Defaults to 800.
+                (default: 800)
             height (int, optional): the height of you chart in pixels.
-                Defaults to 600.
+                (default: 600)
             tools (bool, optional): to enable or disable the tools in
-                your chart. Defaults to True
+                your chart. (default: True)
             filename (str or bool, optional): the name of the file where
                 your chart. will be written. If you pass True to this
                 argument, it will use ``untitled`` as a filename.
-                Defaults to False.
+                (default: False)
             server (str or bool, optional): the name of your chart in
                 the server. If you pass True to this argument, it will
                 use ``untitled`` as the name in the server.
-                Defaults to False.
-            notebook (bool, optional): whether to output to IPython notebook
                 (default: False)
+            notebook (bool, optional): whether to output to IPython
+                notebook (default: False)
             facet (bool, optional): generate multiple areas on multiple
-                separate charts for each series if True. Defaults to
-                False
+                separate charts for each series if True. (default: False)
             xgrid (bool, optional): whether to display x grid lines
                 (default: True)
             ygrid (bool, optional): whether to display y grid lines
@@ -127,6 +132,7 @@ def __init__(self, values, bins, mu=None, sigma=None,
         self.bins = bins
         self.mu = mu
         self.sigma = sigma
+        self.density = density
         self.source = None
         self.xdr = None
         self.ydr = None
@@ -161,7 +167,7 @@ def get_data(self):
             self.set_and_get("", val, self.values[val])
             #build the histogram using the set bins number
             hist, edges = np.histogram(
-                np.array(self.data[val]), density=True, bins=self.bins
+                np.array(self.data[val]), density=self.density, bins=self.bins
             )
             self.set_and_get("hist", val, hist)
             self.set_and_get("edges", val, edges)


Test patch:
diff --git a/bokeh/tests/test_charts.py b/bokeh/tests/test_charts.py
--- a/bokeh/tests/test_charts.py
+++ b/bokeh/tests/test_charts.py
@@ -729,6 +729,35 @@ def test_supported_input(self):
                 key = key.replace('lognormal', '1').replace('normal', '0')
                 assert_array_almost_equal(hm.data[key], expected_v, decimal=2)
 
+    @patch('bokeh.charts.histogram.np.histogram', return_value=('a', 'b'))
+    def test_histogram_params(self, histogram_mock):
+        inputs = [[5, 0, 0.5, True], [3, 1, 0, False]]
+        normal = [1, 2, 3, 1]
+        lognormal = [5, 4, 4, 1]
+        xyvalues = OrderedDict()
+        xyvalues['normal'] = normal
+        xyvalues['lognormal'] = lognormal
+
+        for (bins, mu, sigma, dens) in inputs:
+            histogram_mock.reset_mock()
+            kws = dict(bins=bins, mu=mu, sigma=sigma, density=dens)
+            hm = create_chart(Histogram, xyvalues, compute_values=False, **kws)
+
+            # ensure all class attributes have been correctly set
+            for key, value in kws.items():
+                self.assertEqual(getattr(hm, key), value)
+
+            hm.get_data()
+            # ensure we are calling numpy.histogram with the right args
+            calls = histogram_mock.call_args_list
+            assert_array_equal(calls[0][0][0], np.array([1, 2, 3, 1]))
+            assert_array_equal(calls[1][0][0], np.array([5, 4, 4, 1]))
+            self.assertEqual(calls[0][1]['bins'], bins)
+            self.assertEqual(calls[1][1]['bins'], bins)
+            self.assertEqual(calls[0][1]['density'], dens)
+            self.assertEqual(calls[1][1]['density'], dens)
+
+
 class TestLine(unittest.TestCase):
     def test_supported_input(self):
         xyvalues = OrderedDict()