Repository: bokeh/bokeh

Issue:
Feature Request: Error Bar Plots
For scientists one hugely important feature would be plots with error bars und lower/upper limits like these:

http://matplotlib.org/examples/statistics/errorbar_limits.html

Also see this stackexchange question:
http://stackoverflow.com/questions/29166353/how-do-you-add-error-bars-to-bokeh-plots-in-python



Discussion:
Any news on this?

:+1: 

:+1:  

And thanks to @MaxNoe for the SO answer, that will do until it gets built in.

Now looking into it more closely, the SO solution is not working with interactive plots. Back to square one.

What's the problem?

```
errorbar(pp, x='hjd', y='flux', xerr='fluxerr', source=s2)

​source.callback = CustomJS(args=dict(s2=s2), code="""
        var inds = cb_obj.get('selected')['1d'].indices;
        var d1 = cb_obj.get('data');

​        s2.get('data')['hjd'] = d1['hjd'][inds[0]]
        s2.get('data')['flux'] = d1['flux'][inds[0]]
        s2.get('data')['fluxerr'] = d1['fluxerr'][inds[0]]

        cb_obj.trigger('change');
        s2.trigger('change');
    """)

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-38-524c4b97c72f> in <module>()
     30 #    pp.line(hjd[i], flux[i])
     31 
---> 32 errorbar(pp, x='hjd', y='flux', xerr='fluxerr', source=s2)
     33 
     34 source.callback = CustomJS(args=dict(s2=s2), code="""

<ipython-input-29-ce33a5d4f725> in errorbar(fig, x, y, xerr, yerr, color, source, point_kwargs, error_kwargs)
      7       x_err_y = []
      8       for px, py, err in zip(x, y, xerr):
----> 9           x_err_x.append((px - err, px + err))
     10           x_err_y.append((py, py))
     11       fig.multi_line(x_err_x, x_err_y, color=color, source=source, **error_kwargs)

TypeError: unsupported operand type(s) for -: 'str' and 'str'

```

You need to give it the data as something iterable.

+1

+1

+1

+1

+1
@bokeh/dev, has there been consideration as to plotting errorbars?  Would this be best in `plotting.figure.errorbar`, i.e., as a glyph similar to circle?  

This method produces a nice result by wrapping existing code:
```
def errorbar(fig, x, y, xerr=None, yerr=None, color='red', 
             point_kwargs={}, error_kwargs={}):

    fig.circle(x, y, color=color, **point_kwargs)

    if xerr is not None:
        x_err_x = []
        x_err_y = []
        for px, py, err in zip(x, y, xerr):
            x_err_x.append((px - err, px + err))
            x_err_y.append((py, py))
        fig.multi_line(x_err_x, x_err_y, color=color, **error_kwargs)

    if yerr is not None:
        y_err_x = []
        y_err_y = []
        for px, py, err in zip(x, y, yerr):
            y_err_x.append((px, px))
            y_err_y.append((py - err, py + err))
        fig.multi_line(y_err_x, y_err_y, color=color, **error_kwargs)
```

I am thinking to try to implement this though it may take me a few weeks to get the time to fully complete it.  Please comment with any thoughts/recommendations.
I think it should be 2 variant: user pass errors as param, and they are calculating automatically by code.
So I think there are a couple of levels here. Give +/- error values there are a variety of ways to indicate them visually:

* lines (stems) drawn between the +/- endpoints
* markers or segments (whiskers) drawn at the +/- endpoints 
* both of the above
* a filled area between the +/- sides

I think people will want all these ways, so any solution should cater to all of them.  

Given that, the first thing is that I think there is a missing useful `Annotation`, which is 

* `FilledArea`

Given two series and a dimension, (horizontal or vertical) it will draw a filled area between the two series. This differs from `PolyAnnotation` in that it specifically fills *between* two series, it's not an arbitrary polygon. It could perhaps be a subclass of `PolyAnnotation` to re-use implementation, though. This is useful on its own and could easily be a standalone PR. 

It might also make sense to have `Stem` and `Whisker` as new annotations too. Then we could have a  higher level function `bokeh.plotting.errorbars` or something that takes the `upper` and `lower` error, some description of the kind of visual desired, and whatever other configuration is needed and constructs the appropriate lower level annotations to add to a plot.  

This is all very rough at the moment, though. Thoughts?
I think we'd want new `Annotations`, btw so that we could send `upper` and `lower` as actual data to BokehJS. If we do that, it opens up great possibilities with the new data spec transforms:
```
FilledArea(upper={'field': 'y':, 'transform': ADD_FIELD('upper_error')}, ...
           source=source)
```
Then (pending one open issue) if the `y` data or 'upper_error' columns were changed, everything would adjust automatically.
Lastly, I don't think at this point we should be automatically *computing* error bars. That is something for higher level facilities that are close to the original input data for an analysis to generate. 
@bryevdv 
About error computation. I think calculating standard deviation is enough. Also it is possible to pass function for error computing as a parameter. Mb this is even better: user can pass list of int (errors values) or function for error calculating.
@ArtyomKaltovich respectfully I still maintain my earlier statement. At this point, looking towards a 1.0 release, we are actively looking for ways to *shrink* the library, reduce dependencies, and present a smaller testing surface, in order to make Bokeh a rock-solid, more easily maintainable foundation for other tools to build upon at higher levels. 
Yeah, and than create this :)
>So I think there are a couple of levels here. Give +/- error values there are a variety of ways to indicate them visually:

>    lines (stems) drawn between the +/- endpoints
>    markers or segments (whiskers) drawn at the +/- endpoints
>    both of the above
>    a filled area between the +/- sides

Anyway, creating plots like this http://seaborn.pydata.org/_images/seaborn-barplot-1.png is important feature, and many scientists use it a lot.
So, mb it is better to add error parameter to bar() and circle() methods of figure, instead of creating separated methods.


I very much agree with Bryan here -- there are lots of ways of defining
error bars, and just supporting one (e.g. stddev) is not going to be very
useful.  Bokeh is a low-level library, and it makes sense to simply pass in
the error bars lengths, computed however is appropriate for your field and
for your specific situation, and then Bokeh can handle plotting them.

Jim

On Sat, Feb 4, 2017 at 1:22 PM, Bryan Van de Ven <notifications@github.com>
wrote:

> @ArtyomKaltovich <https://github.com/ArtyomKaltovich> respectfully I
> still maintain my earlier statement. At this point, looking towards a 1.0
> release, we are actively looking for ways to *shrink* the library, reduce
> dependencies, and present a smaller testing surface, in order to make Bokeh
> a rock-solid, more easily maintainable foundation for other tools to build
> upon at higher levels.
>
> —
> You are receiving this because you are on a team that was mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/bokeh/bokeh/issues/2352#issuecomment-277468374>, or mute
> the thread
> <https://github.com/notifications/unsubscribe-auth/ABnfCML_1e9kUOm9hMwJxAapaNSlQ58Gks5rZM_kgaJpZM4Evwml>
> .
>

> I don't think at this point we should be automatically computing error bars. That is something for higher level facilities that are close to the original input data for an analysis to generate.

> there are lots of ways of defining error bars, and just supporting one (e.g. stddev) is not going to be very useful

I completely agree.  There is too much misunderstanding about error estimates, particularly the difference between standard deviation and standard error of the mean.  

Automating the error calculation separates the user from the computation process, making it more likely they will not accurately represent the steps taken when presenting the data.  This would effectively move responsibility from the user to the plotting package, requiring them to lookup either in the docs or the code what they are actually representing.    

It is a simple thing for users to provide whatever error they want to show.  
I second priority on user being able to pass in the errors. Much more important than any automatic calculation that just prevents the user from carefully thinking about what errors actually are. 
I also think bokeh should only do the visualisation part, not the statistics part.
I'd like to get to this in `0.12.6`. To summarize the current plan as I understand it. Three new annotations:

* `FilledArea`
* `Stem` 
* `Whisker`


Arrows would also be great to mark upper and lower limits
Arrows already exist 
Just to be clear, are these examples what you are referring to?
- `FilledArea`
![filled](https://www.quentinhuys.com/pictures/errorbar.jpg)
- `Stem`
![stem](http://matplotlib.org/_images/errorbar_demo2.png)
- `Whisker`
![whisker](https://root.cern.ch/root/html534/guides/users-guide/pictures/03000050.png)
Some considerations to keep in mind for the `Whisker` option, I have seen some plotting tools (Matplotlib or Matlab) make the Whisker width much larger when plot on a logarithmic axis.  This can obscure the data.  A simple solution is to switch to the `Stem` method of showing errorbars.  A more useful default would be that the whisker width consistently depends on the number of pixels in the corresponding axis range, i.e., the same width on log/linear and the width does get larger/smaller when zooming in/out.  Also, having control on the width of the whiskers (maybe as a multiplicative scale factor to the default value) could be useful.
In addition to being able to add errorbars to lines and scatter plots, it would be worthwhile to add them to the top (or potentially the bottom?) of vertical bar charts (or the right/left for horizontal bar charts).  This should be able to reuse most of the same code, just center the errorbars at the edge of the glyph.
![bar chart error](https://plot.ly/~PlotBot/51/control-vs-experimental.png) 
Is it possible to achieve this with the current arrow annotation?
(Mixed arrows / whiskers / stem)

![img](http://matplotlib.org/_images/errorbar_limits.png)
Another thing to keep in mind, related to log plots, is that when changing to a logarithmic axis, the errorbars should be rescaled to the logarithmic reference frame accordingly.  Mathematically, we scale from the `y` frame to the `y'` frame as 
```
y' = log(y) = ln(y) / ln(10)
```
Similarly, the error in `y`, we will use `dy`, is also transformed
```
dy'/dy = d/dy log(y) = d/dy ln(y) / ln(10)
dy'/dy = (1/y) / 2.303 = 0.434 / y
dy' = 0.434 dy / y
```
So instead of using `dy'`, the error should be `0.434 dy / y`.

[Here](http://faculty.washington.edu/stuve/uwess/log_error.pdf) is a bit more in depth explanation.
@MaxNoe Absolutely, and with the `CustomJSTransform` in `0.12.5` you can compute the end points from the data points in the browser. 

@StevenCHowell that's roughly the breakdown, though for `Whisker` I would imagine just the whisker part, not the stem as well. 

The log considerations are important, thanks for bringing them up. It may necessary to just get a basic functionality added first and add refinements later. 
> for Whisker I would imagine just the whisker part, not the stem as well.

Personally, I have not seen this used before (though I did find someone who [made the stems dashed](https://vaadin.com/vaadin-fw8-documentation-portlet/charts/webcomponents-api/img/charts-errorbar.png)).  @bryevdv, have you seen whiskers w/o stems much?  I wonder if the error limits could get confused as another data set or other points (though if they are close enough together in `x` it would start to look like dashed line resembling the `FilledArea` option).

FWIW, the combined whisker and stem representation was the default in Matplotlib 1.5 (2.0 dropped the whiskers) and also [Matlab](https://www.mathworks.com/help/matlab/ref/errorbar.html).  It may surprise/confuse users more familiar with the combined representation to see just whiskers, or to have to use both `Whisker` and `Stem` to get what seems a pretty standard representation.  
Personally, I think that for most datasets the stem representation is less cluttered and sufficient. It seems others share this preference as Matplotlib changed their default.  
To achieve 
> Mixed arrows / whiskers / stem

what would the API be like?

Would there be a unified `errorbar` method
```
p = bokeh.plotting.figure()
p.errorbar(x, y, xerr=xerr, yerr=yerr, xerr_style=x_style_array, yerr_style=y_style_array)
``` 
or would it use boolean arrays with a separate method for each errorbar type?
```
# define a mask to select how to display the errors
whisker_mask = np.array([0, 0, 1, 0, 1, 0, 0, 0, 1, 0], dtype=bool)
arrow_mask = ~whiskers

p1 = bokeh.plotting.figure()
p1.Whiskers(x1[whisker_mask], y1[whisker_mask], yerr=yerr[whisker_mask])
p1.Arrow(x2[arrow_mask], y2[arrow_mask], yerr=yerr[arrow_mask])

p2 = bokeh.plotting.figure()
p2.line(x3, y3, y_whiskers=yerr[whisker_mask], y_arrows=[arrow_mask])
```
Note that to get the one directional arrows, @MaxNoe linked to, [Matplotlib uses `uplims` and `lolims` kwargs](http://matplotlib.org/examples/statistics/errorbar_limits.html).
> Personally, I have not seen this used before (though I did find someone who made the stems dashed). @bryevdv, have you seen whiskers w/o stems much? 

This concerns the set of low level models that is needed to support all the use cases. Higher level APIs could assemble them in other ways or in combination
relevant comment from another issue https://github.com/bokeh/bokeh/pull/5349#issuecomment-294331706

> since I am currently drawing points/errorbars as two separate sets of glyps, the hiding only half works, but I am guessing this functionality will work with the upcoming FilledArea/Whisker/Stem classes?
I have a colleague who just came to me trying to draw whisker errorbars.  Though somewhat of a hack, FWIW I modified the [`MultiLine`](http://bokeh.pydata.org/en/latest/docs/reference/models/glyphs/multi_line.html) example to help him out.  I changed the definition of `xpts` and `ypts` using
```
h = 2.0/3
w = 1.0/3
xpts = np.array([0, w, w/2, w/2, 0, w])
ypts = np.array([0, 0, 0, h, h, h])
```
to produce this
![image](https://cloud.githubusercontent.com/assets/9484229/25149967/62a0c310-244e-11e7-94c6-c2d5ac69aa97.png)
It's a small step to customizing each one to his data.

Resolving patch:
diff --git a/bokeh/models/annotations.py b/bokeh/models/annotations.py
--- a/bokeh/models/annotations.py
+++ b/bokeh/models/annotations.py
@@ -983,3 +983,57 @@ class Tooltip(Annotation):
     show_arrow = Bool(default=True, help="""
     Whether tooltip's arrow should be showed.
     """)
+
+# This only exists to prevent a circular import.
+def _DEFAULT_TEE():
+    from .arrow_heads import TeeHead
+    return TeeHead(level="underlay", size=10)
+
+class Whisker(Annotation):
+    ''' Render a whisker along a dimension.
+
+    '''
+
+    lower = DistanceSpec(help="""
+    The coordinates of the lower end of the whiskers.
+    """)
+
+    lower_head = Instance('.models.arrow_heads.ArrowHead', default=_DEFAULT_TEE, help="""
+    Instance of ArrowHead.
+    """)
+
+    upper = DistanceSpec(help="""
+    The coordinations of the upper end of the whiskers.
+    """)
+
+    upper_head = Instance('.models.arrow_heads.ArrowHead', default=_DEFAULT_TEE, help="""
+    Instance of ArrowHead.
+    """)
+
+    base = DistanceSpec(help="""
+    The orthogonal coordinates of the upper and lower values.
+    """)
+
+    dimension = Enum(Dimension, default='height', help="""
+    The direction of the band.
+    """)
+
+    source = Instance(DataSource, default=lambda: ColumnDataSource(), help="""
+    Local data source to use when rendering annotations on the plot.
+    """)
+
+    x_range_name = String('default', help="""
+    A particular (named) x-range to use for computing screen locations when
+    rendering annotations on the plot. If unset, use the default x-range.
+    """)
+
+    y_range_name = String('default', help="""
+    A particular (named) y-range to use for computing screen locations when
+    rendering annotations on the plot. If unset, use the default y-range.
+    """)
+
+    line_props = Include(LineProps, use_prefix=False, help="""
+    The %s values for the whisker body.
+    """)
+
+    level = Override(default="underlay")
diff --git a/bokeh/models/arrow_heads.py b/bokeh/models/arrow_heads.py
--- a/bokeh/models/arrow_heads.py
+++ b/bokeh/models/arrow_heads.py
@@ -48,6 +48,19 @@ class NormalHead(ArrowHead):
 
     fill_color = Override(default="black")
 
+class TeeHead(ArrowHead):
+    ''' Render a tee-style arrow head.
+
+    '''
+
+    size = Float(default=25, help="""
+    The size, in pixels, of the arrow head.
+    """)
+
+    line_props = Include(LineProps, use_prefix=False, help="""
+    The %s values for the arrow head outline.
+    """)
+
 class VeeHead(ArrowHead):
     ''' Render a vee-style arrow head.
 
diff --git a/sphinx/source/docs/user_guide/examples/plotting_whisker.py b/sphinx/source/docs/user_guide/examples/plotting_whisker.py
new file mode 100644
--- /dev/null
+++ b/sphinx/source/docs/user_guide/examples/plotting_whisker.py
@@ -0,0 +1,30 @@
+from bokeh.models import ColumnDataSource, Whisker
+from bokeh.plotting import figure, show
+from bokeh.sampledata.autompg import autompg as df
+
+colors = ["red", "olive", "darkred", "goldenrod", "skyblue", "orange", "salmon"]
+
+p = figure(plot_width=600, plot_height=300, title="Years vs mpg with Quartile Ranges")
+
+base, lower, upper = [], [], []
+
+for i, year in enumerate(list(df.yr.unique())):
+    year_mpgs = df[df['yr'] == year]['mpg']
+    mpgs_mean = year_mpgs.mean()
+    mpgs_std = year_mpgs.std()
+    lower.append(mpgs_mean - mpgs_std)
+    upper.append(mpgs_mean + mpgs_std)
+    base.append(year)
+
+source_error = ColumnDataSource(data=dict(base=base, lower=lower, upper=upper))
+
+p.add_layout(
+    Whisker(source=source_error, base="base", upper="upper", lower="lower")
+)
+
+for i, year in enumerate(list(df.yr.unique())):
+    y = df[df['yr'] == year]['mpg']
+    color = colors[i % len(colors)]
+    p.circle(x=year, y=y, color=color)
+
+show(p)


Test patch:
diff --git a/bokeh/models/tests/test_annotations.py b/bokeh/models/tests/test_annotations.py
--- a/bokeh/models/tests/test_annotations.py
+++ b/bokeh/models/tests/test_annotations.py
@@ -5,7 +5,7 @@
 from bokeh.core.validation import check_integrity
 from bokeh.models.annotations import (
     Legend, LegendItem, ColorBar, Arrow, BoxAnnotation, Span, LabelSet, Label,
-    Title, Band
+    Title, Band, Whisker
 )
 from bokeh.models import (
     ColumnDataSource, ArrowHead, BasicTicker, BasicTickFormatter, GlyphRenderer
@@ -352,6 +352,45 @@ def test_Title():
         prefix('background_', FILL))
 
 
+def test_Whisker():
+    whisker = Whisker()
+    assert whisker.plot is None
+    assert whisker.level == 'underlay'
+    assert whisker.lower is None
+    assert whisker.lower_units == 'data'
+    assert isinstance(whisker.lower_head, ArrowHead)
+    assert whisker.lower_head.size == 10
+    assert whisker.lower_head.level == 'underlay'
+    assert whisker.upper is None
+    assert whisker.upper_units == 'data'
+    assert isinstance(whisker.upper_head, ArrowHead)
+    assert whisker.upper_head.size == 10
+    assert whisker.upper_head.level == 'underlay'
+    assert whisker.base is None
+    assert whisker.dimension == 'height'
+    assert isinstance(whisker.source, ColumnDataSource)
+    assert whisker.x_range_name == 'default'
+    assert whisker.y_range_name == 'default'
+    check_line_properties(whisker, "")
+    check_properties_existence(whisker, [
+        "plot",
+        "visible",
+        "level",
+        "lower",
+        "lower_units",
+        "lower_head",
+        "upper",
+        "upper_units",
+        "upper_head",
+        "base",
+        "base_units",
+        "dimension",
+        "source",
+        "x_range_name",
+        "y_range_name"],
+        LINE)
+
+
 def test_can_add_multiple_glyph_renderers_to_legend_item():
     legend_item = LegendItem()
     gr_1 = GlyphRenderer()
diff --git a/tests/integration/annotations/screenshots/base__test_arrow__test_arrow.png b/tests/integration/annotations/screenshots/base__test_arrow__test_arrow.png
Binary files a/tests/integration/annotations/screenshots/base__test_arrow__test_arrow.png and b/tests/integration/annotations/screenshots/base__test_arrow__test_arrow.png differ
diff --git a/tests/integration/annotations/screenshots/base__test_whisker__test_whisker.png b/tests/integration/annotations/screenshots/base__test_whisker__test_whisker.png
new file mode 100644
Binary files /dev/null and b/tests/integration/annotations/screenshots/base__test_whisker__test_whisker.png differ
diff --git a/tests/integration/annotations/test_arrow.py b/tests/integration/annotations/test_arrow.py
--- a/tests/integration/annotations/test_arrow.py
+++ b/tests/integration/annotations/test_arrow.py
@@ -1,7 +1,7 @@
 from __future__ import absolute_import
 
 from bokeh.io import save
-from bokeh.models import Arrow, OpenHead, NormalHead, VeeHead
+from bokeh.models import Arrow, OpenHead, NormalHead, TeeHead, VeeHead
 from bokeh.plotting import figure
 from tests.integration.utils import has_no_console_errors
 
@@ -17,13 +17,13 @@ def test_arrow(output_file_url, selenium, screenshot):
     # Have to specify x/y range as labels aren't included in the plot area solver
     plot = figure(height=HEIGHT, width=WIDTH, x_range=(0,10), y_range=(0,10), tools='', toolbar_location="above")
 
-    arrow1 = Arrow(x_start=1, y_start=3, x_end=6, y_end=8,
+    arrow1 = Arrow(x_start=1, y_start=4, x_end=6, y_end=9,
                    line_color='green', line_alpha=0.7,
                    line_dash='8 4', line_width=5, end=OpenHead()
                    )
     arrow1.end.line_width=8
 
-    arrow2 = Arrow(x_start=2, y_start=2, x_end=7, y_end=7,
+    arrow2 = Arrow(x_start=2, y_start=3, x_end=7, y_end=8,
                    start=NormalHead(), end=VeeHead()
                    )
     arrow2.start.fill_color = 'indigo'
@@ -34,6 +34,7 @@ def test_arrow(output_file_url, selenium, screenshot):
     plot.add_layout(arrow2)
 
      # test arrow body clipping
+    plot.add_layout(Arrow(start=TeeHead(line_width=1), x_start=6, y_start=5, x_end=8, y_end=6, line_width=10))
     plot.add_layout(Arrow(start=VeeHead(line_width=1, fill_color="white"), x_start=6, y_start=4, x_end=8, y_end=5, line_width=10))
     plot.add_layout(Arrow(start=NormalHead(line_width=1, fill_color="white"), x_start=6, y_start=3, x_end=8, y_end=4, line_width=10))
     plot.add_layout(Arrow(start=OpenHead(line_width=1), x_start=6, y_start=2, x_end=8, y_end=3, line_width=10))
diff --git a/tests/integration/annotations/test_whisker.py b/tests/integration/annotations/test_whisker.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/annotations/test_whisker.py
@@ -0,0 +1,50 @@
+from __future__ import absolute_import
+
+from bokeh.io import save
+from bokeh.models import Plot, Range1d, Whisker, OpenHead, ColumnDataSource
+from tests.integration.utils import has_no_console_errors
+
+import pytest
+pytestmark = pytest.mark.integration
+
+HEIGHT = 600
+WIDTH = 600
+
+@pytest.mark.screenshot
+def test_whisker(output_file_url, selenium, screenshot):
+
+    x_range = Range1d(0, 10)
+    y_range = Range1d(0, 10)
+
+    # Have to specify x/y range as labels aren't included in the plot area solver
+    plot = Plot(plot_height=HEIGHT, plot_width=WIDTH,
+                x_range=x_range, y_range=y_range, toolbar_location=None)
+
+    source = ColumnDataSource(data=dict(
+        x1 = [1,3,5,7,9],
+        lower1 = [1,2,1,2,1],
+        upper1 = [2,3,2,3,2],
+        x2 = [200, 250, 350, 450, 550],
+        lower2 = [400, 300, 400, 300, 400],
+        upper2 = [500, 400, 500, 400, 500],
+    ))
+
+    whisker1 = Whisker(base='x1', lower='lower1', upper='upper1',
+                       line_width=3, line_color='red', line_dash='dashed',
+                       source=source)
+
+    whisker2 = Whisker(base='x2', lower='lower2', upper='upper2', upper_head=OpenHead(),
+                       base_units='screen', lower_units='screen', upper_units='screen',
+                       dimension='width', line_width=3, line_color='green',
+                       source=source)
+
+    plot.add_layout(whisker1)
+    plot.add_layout(whisker2)
+
+    # Save the plot and start the test
+    save(plot)
+    selenium.get(output_file_url)
+    assert has_no_console_errors(selenium)
+
+    # Take screenshot
+    screenshot.assert_is_valid()