Skip to content

Commit

Permalink
Switched to Whisker and Band glyphs for ErrorBars and Spread (#1915)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored and jlstevens committed Sep 25, 2017
1 parent f578738 commit 1c93aab
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 84 deletions.
2 changes: 1 addition & 1 deletion holoviews/plotting/bokeh/__init__.py
Expand Up @@ -145,7 +145,7 @@ def colormap_generator(palette):
options.Bars = Options('style', color=Cycle(), line_color='black', width=0.8)

options.Spikes = Options('style', color='black', cmap='fire')
options.Area = Options('style', color=Cycle(), line_color='black')
options.Area = Options('style', color=Cycle(), alpha=1, line_color='black')
options.VectorField = Options('style', color='black')

# Paths
Expand Down
161 changes: 80 additions & 81 deletions holoviews/plotting/bokeh/chart.py
Expand Up @@ -3,7 +3,7 @@
import numpy as np
import param
from bokeh.models import (DataRange1d, CategoricalColorMapper, CustomJS,
HoverTool, FactorRange)
HoverTool, FactorRange, Whisker, Band)
from bokeh.models.tools import BoxSelectTool

from ...core import Dataset, OrderedDict
Expand All @@ -16,7 +16,7 @@
from .element import (ElementPlot, ColorbarPlot, LegendPlot, CompositeElementPlot,
line_properties, fill_properties)
from .path import PathPlot, PolygonPlot
from .util import bokeh_version, expand_batched_style, categorize_array, rgb2hex
from .util import bokeh_version, expand_batched_style, categorize_array, rgb2hex, mpl_to_bokeh


class PointPlot(LegendPlot, ColorbarPlot):
Expand Down Expand Up @@ -300,61 +300,6 @@ def get_batched_data(self, overlay, ranges=None, empty=False):
return data, mapping


class AreaPlot(PolygonPlot):

def get_extents(self, element, ranges):
vdims = element.vdims
vdim = vdims[0].name
if len(vdims) > 1:
ranges[vdim] = max_range([ranges[vd.name] for vd in vdims])
else:
vdim = vdims[0].name
ranges[vdim] = (np.nanmin([0, ranges[vdim][0]]), ranges[vdim][1])
return super(AreaPlot, self).get_extents(element, ranges)

def get_data(self, element, ranges=None, empty=False):
mapping = dict(self._mapping)
if empty: return {'xs': [], 'ys': []}
xs = element.dimension_values(0)
x2 = np.hstack((xs[::-1], xs))

if len(element.vdims) > 1:
bottom = element.dimension_values(2)
else:
bottom = np.zeros(len(element))
ys = np.hstack((bottom[::-1], element.dimension_values(1)))

if self.invert_axes:
data = dict(xs=[ys], ys=[x2])
else:
data = dict(xs=[x2], ys=[ys])
return data, mapping


class SpreadPlot(PolygonPlot):

style_opts = line_properties + fill_properties

def get_data(self, element, ranges=None, empty=None):
if empty:
return dict(xs=[], ys=[]), dict(self._mapping)

xvals = element.dimension_values(0)
mean = element.dimension_values(1)
neg_error = element.dimension_values(2)
pos_idx = 3 if len(element.dimensions()) > 3 else 2
pos_error = element.dimension_values(pos_idx)

lower = mean - neg_error
upper = mean + pos_error
band_x = np.append(xvals, xvals[::-1])
band_y = np.append(lower, upper[::-1])
if self.invert_axes:
data = dict(xs=[band_y], ys=[band_x])
else:
data = dict(xs=[band_x], ys=[band_y])
return data, dict(self._mapping)


class HistogramPlot(ElementPlot):

Expand Down Expand Up @@ -455,39 +400,93 @@ def _init_glyph(self, plot, mapping, properties):
return ret


class ErrorPlot(PathPlot):

horizontal = param.Boolean(default=False)
class ErrorPlot(ElementPlot):

style_opts = line_properties

_mapping = dict(base="base", upper="upper", lower="lower")

_plot_methods = dict(single=Whisker)

def get_data(self, element, ranges=None, empty=False):
if empty:
return dict(xs=[], ys=[]), dict(self._mapping)

data = element.array(dimensions=element.dimensions()[0:4])
err_xs = []
err_ys = []
for row in data:
x, y = row[0:2]
if len(row) > 3:
neg, pos = row[2:]
else:
neg, pos = row[2], row[2]
mapping = dict(self._mapping)
base = element.dimension_values(0)
ys = element.dimension_values(1)
if len(element.vdims) > 2:
neg, pos = (element.dimension_values(vd) for vd in element.vdims[1:3])
lower, upper = ys-neg, ys+pos
else:
err = element.dimension_values(2)
lower, upper = ys-err, ys+err
data = dict(base=base, lower=lower, upper=upper)

if self.horizontal:
err_xs.append((x - neg, x + pos))
err_ys.append((y, y))
else:
err_xs.append((x, x))
err_ys.append((y - neg, y + pos))
if self.invert_axes:
mapping['dimension'] = 'width'
else:
mapping['dimension'] = 'height'
self._categorize_data(data, ('base',), element.dimensions())
return (data, mapping)


def _init_glyph(self, plot, mapping, properties):
"""
Returns a Bokeh glyph object.
"""
properties.pop('legend', None)
for prop in ['color', 'alpha']:
if prop not in properties:
continue
pval = properties.pop(prop)
line_prop = 'line_%s' % prop
fill_prop = 'fill_%s' % prop
if line_prop not in properties:
properties[line_prop] = pval
if fill_prop not in properties and fill_prop in self.style_opts:
properties[fill_prop] = pval
properties = mpl_to_bokeh(properties)
plot_method = self._plot_methods['single']
glyph = plot_method(**dict(properties, **mapping))
plot.add_layout(glyph)
return None, glyph



class SpreadPlot(ErrorPlot):

style_opts = line_properties + fill_properties
_plot_methods = dict(single=Band)



class AreaPlot(SpreadPlot):

def get_extents(self, element, ranges):
vdims = element.vdims
vdim = vdims[0].name
if len(vdims) > 1:
ranges[vdim] = max_range([ranges[vd.name] for vd in vdims])
else:
vdim = vdims[0].name
ranges[vdim] = (np.nanmin([0, ranges[vdim][0]]), ranges[vdim][1])
return super(AreaPlot, self).get_extents(element, ranges)

def get_data(self, element, ranges=None, empty=False):
xs = element.dimension_values(0)
if len(element.vdims) > 1:
lower = element.dimension_values(2)
else:
lower = np.zeros(len(element))
upper = element.dimension_values(1)
data = dict(base=xs, upper=upper, lower=lower)

mapping = dict(self._mapping)
if self.invert_axes:
data = dict(xs=err_ys, ys=err_xs)
mapping['dimension'] = 'width'
else:
data = dict(xs=err_xs, ys=err_ys)
self._categorize_data(data, ('xs', 'ys'), element.dimensions())
return (data, dict(self._mapping))
mapping['dimension'] = 'height'
return data, mapping



class SpikesPlot(PathPlot, ColorbarPlot):
Expand Down
4 changes: 2 additions & 2 deletions tests/testplotinstantiation.py
Expand Up @@ -1142,8 +1142,8 @@ def test_points_errorbars_text_ndoverlay_categorical_xaxis(self):
self.assertEqual(x_range.factors, ['A', 'B', 'C', 'D', 'E'])
self.assertIsInstance(y_range, Range1d)
error_plot = plot.subplots[('ErrorBars', 'I')]
for xs, factor in zip(error_plot.handles['source'].data['xs'], factors):
self.assertEqual([factor, factor], xs)
for xs, factor in zip(error_plot.handles['source'].data['base'], factors):
self.assertEqual(factor, xs)

def test_points_errorbars_text_ndoverlay_categorical_xaxis_invert_axes(self):
overlay = NdOverlay({i: Points(([chr(65+i)]*10,np.random.randn(10)))
Expand Down

0 comments on commit 1c93aab

Please sign in to comment.