diff --git a/examples/reference/elements/bokeh/BoxWhisker.ipynb b/examples/reference/elements/bokeh/BoxWhisker.ipynb index d2d5aec659..38e7a97b89 100644 --- a/examples/reference/elements/bokeh/BoxWhisker.ipynb +++ b/examples/reference/elements/bokeh/BoxWhisker.ipynb @@ -28,7 +28,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A ``BoxWhisker`` Element is a quick way of visually summarizing one or more groups of numerical data through their quartiles. \n", + "A ``BoxWhisker`` Element is a quick way of visually summarizing one or more groups of numerical data through their quartiles. The boxes of a ``BoxWhisker`` element represent the first, second and third quartiles. The whiskers follow the Tukey boxplot definition representing the lowest datum still within 1.5 IQR of the lower quartile, and the highest datum still within 1.5 IQR of the upper quartile. Any points falling outside this range are shown as distinct outlier points.\n", "\n", "The data of a ``BoxWhisker`` Element may have any number of key dimensions representing the grouping of the value dimension and a single value dimensions representing the distribution of values within each group. See the [Tabular Datasets](../../../user_guide/07-Tabular_Datasets.ipynb) user guide for supported data formats, which include arrays, pandas dataframes and dictionaries of arrays." ] diff --git a/examples/reference/elements/matplotlib/BoxWhisker.ipynb b/examples/reference/elements/matplotlib/BoxWhisker.ipynb index 5520223d76..abfe2d1cea 100644 --- a/examples/reference/elements/matplotlib/BoxWhisker.ipynb +++ b/examples/reference/elements/matplotlib/BoxWhisker.ipynb @@ -28,7 +28,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "A ``BoxWhisker`` Element is a quick way of visually summarizing one or more groups of numerical data through their quartiles. \n", + "A ``BoxWhisker`` Element is a quick way of visually summarizing one or more groups of numerical data through their quartiles. The boxes of a ``BoxWhisker`` element represent the first, second and third quartiles. The whiskers follow the Tukey boxplot definition representing the lowest datum still within 1.5 IQR of the lower quartile, and the highest datum still within 1.5 IQR of the upper quartile. Any points falling outside this range are shown as distinct outlier points.\n", "\n", "The data of a ``BoxWhisker`` Element may have any number of key dimensions representing the grouping of the value dimension and a single value dimensions representing the distribution of values within each group. See the [Tabular Datasets](../../../user_guide/07-Tabular_Datasets.ipynb) user guide for supported data formats, which include arrays, pandas dataframes and dictionaries of arrays." ] diff --git a/holoviews/element/chart.py b/holoviews/element/chart.py index 3a17deb872..d2d04cea66 100644 --- a/holoviews/element/chart.py +++ b/holoviews/element/chart.py @@ -137,8 +137,14 @@ class Bars(Chart): class BoxWhisker(Chart): """ - BoxWhisker represent data as a distributions highlighting - the median, mean and various percentiles. + BoxWhisker allows representing the distribution of data grouped + into one or more groups by summarizing the data using quartiles. + The boxes of a BoxWhisker element represent the first, second and + third quartiles. The whiskers follow the Tukey boxplot definition + representing the lowest datum still within 1.5 IQR of the lower + quartile, and the highest datum still within 1.5 IQR of the upper + quartile. Any points falling outside this range are shown as + distinct outlier points. """ group = param.String(default='BoxWhisker', constant=True) diff --git a/holoviews/plotting/bokeh/chart.py b/holoviews/plotting/bokeh/chart.py index a0c2843f99..2b1449ef7c 100644 --- a/holoviews/plotting/bokeh/chart.py +++ b/holoviews/plotting/bokeh/chart.py @@ -3,7 +3,8 @@ import numpy as np import param from bokeh.models import (CategoricalColorMapper, CustomJS, HoverTool, - FactorRange, Whisker, Band, Range1d) + FactorRange, Whisker, Band, Range1d, Circle, + VBar, HBar) from bokeh.models.tools import BoxSelectTool from bokeh.transform import jitter @@ -988,6 +989,11 @@ def _get_factors(self, element): xfactors, yfactors = factors, [] return (yfactors, xfactors) if self.invert_axes else (xfactors, yfactors) + def _postprocess_hover(self, renderer, source): + if not isinstance(renderer.glyph, (Circle, VBar, HBar)): + return + super(BoxWhiskerPlot, self)._postprocess_hover(renderer, source) + def get_data(self, element, ranges, style): if element.kdims: groups = element.groupby(element.kdims).data @@ -996,7 +1002,8 @@ def get_data(self, element, ranges, style): vdim = dimension_sanitizer(element.vdims[0].name) # Define CDS data - r1_data, r2_data = ({'index': [], 'top': [], 'bottom': []} for i in range(2)) + r1_data, r2_data = (defaultdict(list, {'index': [], 'top': [], 'bottom': []}) + for i in range(2)) s1_data, s2_data = ({'x0': [], 'y0': [], 'x1': [], 'y1': []} for i in range(2)) w1_data, w2_data = ({'index': [], vdim: []} for i in range(2)) out_data = defaultdict(list, {'index': [], vdim: []}) @@ -1021,6 +1028,7 @@ def get_data(self, element, ranges, style): cidx = element.get_dimension_index(self.color_index) else: cdim, cidx = None, None + hover = any(isinstance(t, HoverTool) for t in self.state.tools) factors = [] for key, g in groups.items(): @@ -1068,9 +1076,15 @@ def get_data(self, element, ranges, style): if len(outliers): out_data['index'] += [label]*len(outliers) out_data[vdim] += list(outliers) - if any(isinstance(t, HoverTool) for t in self.state.tools): + if hover: for kd, k in zip(element.kdims, wrap_tuple(key)): out_data[dimension_sanitizer(kd.name)] += [k]*len(outliers) + if hover: + for kd, k in zip(element.kdims, wrap_tuple(key)): + r1_data[dimension_sanitizer(kd.name)].append(k) + r2_data[dimension_sanitizer(kd.name)].append(k) + r1_data[vdim].append(q2) + r2_data[vdim].append(q2) # Define combined data and mappings bar_glyph = 'hbar' if self.invert_axes else 'vbar' diff --git a/tests/testplotinstantiation.py b/tests/testplotinstantiation.py index ea589eca96..bd45ad042f 100644 --- a/tests/testplotinstantiation.py +++ b/tests/testplotinstantiation.py @@ -1314,12 +1314,22 @@ def test_box_whisker_datetime(self): box = BoxWhisker((times, np.random.rand(len(times))), kdims=['Date']) plot = bokeh_renderer.get_plot(box) formatted = [box.kdims[0].pprint_value(t) for t in times] - if bokeh_version < str('0.12.7'): - formatted = [f.replace(':', ';') for f in formatted] self.assertTrue(all(cds.data['index'][0] in formatted for cds in plot.state.select(ColumnDataSource) if len(cds.data.get('index', [])))) + def test_box_whisker_hover(self): + xs, ys = np.random.randint(0, 5, 100), np.random.randn(100) + box = BoxWhisker((xs, ys), 'A').sort().opts(plot=dict(tools=['hover'])) + plot = bokeh_renderer.get_plot(box) + src = plot.handles['vbar_1_source'] + ys = box.aggregate(function=np.median).dimension_values('y') + hover_tool = plot.handles['hover'] + self.assertEqual(src.data['y'], ys) + self.assertIn(plot.handles['vbar_1glyph_renderer'], hover_tool.renderers) + self.assertIn(plot.handles['vbar_2glyph_renderer'], hover_tool.renderers) + self.assertIn(plot.handles['circle_1glyph_renderer'], hover_tool.renderers) + def test_curve_datetime64(self): dates = [np.datetime64(dt.datetime(2016,1,i)) for i in range(1, 11)] curve = Curve((dates, np.random.rand(10)))