Skip to content

Commit

Permalink
Implemented continuous style mapping for Paths (#3192)
Browse files Browse the repository at this point in the history
  • Loading branch information
philippjfr authored and jlstevens committed Nov 23, 2018
1 parent 87e7ba2 commit 5862f23
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 68 deletions.
9 changes: 5 additions & 4 deletions holoviews/plotting/bokeh/element.py
Expand Up @@ -27,7 +27,7 @@
from ...core import DynamicMap, CompositeOverlay, Element, Dimension
from ...core.options import abbreviated_exception, SkipRendering
from ...core import util
from ...element import Graph, VectorField
from ...element import Graph, VectorField, Path, Contours
from ...streams import Buffer
from ...util.transform import dim
from ..plot import GenericElementPlot, GenericOverlayPlot
Expand Down Expand Up @@ -680,6 +680,9 @@ def _apply_transforms(self, element, source, ranges, style, group=None):

if len(v.ops) == 0 and v.dimension in self.overlay_dims:
val = self.overlay_dims[v.dimension]
elif isinstance(element, Path) and not isinstance(element, Contours):
val = np.concatenate([v.apply(el, ranges=ranges, flat=True)[:-1]
for el in element.split()])
else:
val = v.apply(element, ranges=ranges, flat=True)

Expand All @@ -697,9 +700,7 @@ def _apply_transforms(self, element, source, ranges, style, group=None):
'to the {style} use a groupby operation '
'to overlay your data along the dimension.'.format(
style=k, dim=v.dimension, element=element,
backend=self.renderer.backend
)
)
backend=self.renderer.backend))
elif source.data and len(val) != len(list(source.data.values())[0]):
if isinstance(element, VectorField):
val = np.tile(val, 3)
Expand Down
56 changes: 31 additions & 25 deletions holoviews/plotting/bokeh/path.py
Expand Up @@ -61,9 +61,12 @@ def get_data(self, element, ranges, style):
cdim = element.get_dimension(color)
elif self.color_index is not None:
cdim = element.get_dimension(self.color_index)
style_mapping = any(
s for s, v in style.items() if (s not in self._nonvectorized_styles) and
(isinstance(v, util.basestring) and v in element) or isinstance(v, dim))
inds = (1, 0) if self.invert_axes else (0, 1)
mapping = dict(self._mapping)
if not cdim:
if not cdim and not style_mapping:
if self.static_source:
data = {}
else:
Expand All @@ -72,32 +75,35 @@ def get_data(self, element, ranges, style):
data = dict(xs=xs, ys=ys)
return data, mapping, style

dim_name = util.dimension_sanitizer(cdim.name)
if not self.static_source:
paths = []
vals = {}
hover = 'hover' in self.handles
if hover:
vals = {util.dimension_sanitizer(vd.name): [] for vd in element.vdims}
for path in element.split():
if cdim:
dim_name = util.dimension_sanitizer(cdim.name)
cmapper = self._get_colormapper(cdim, element, ranges, style)
mapping['line_color'] = {'field': dim_name, 'transform': cmapper}
vals[dim_name] = []

paths = []
for path in element.split():
if cdim:
cvals = path.dimension_values(cdim)
array = path.array(path.kdims)
splits = [0]+list(np.where(np.diff(cvals)!=0)[0]+1)
cols = {vd.name: path.dimension_values(vd) for vd in element.vdims}
if len(splits) == 1:
splits.append(len(path))
for (s1, s2) in zip(splits[:-1], splits[1:]):
for i, vd in enumerate(element.vdims):
path_val = cols[vd.name][s1]
vd_column = util.dimension_sanitizer(vd.name)
dt_column = vd_column+'_dt_strings'
vals[vd_column].append(path_val)
if isinstance(path_val, util.datetime_types):
if dt_column not in vals:
vals[dt_column] = []
vals[dt_column].append(vd.pprint_value(path_val))
paths.append(array[s1:s2+1])
xs, ys = ([path[:, idx] for path in paths] for idx in inds)
data = dict(xs=xs, ys=ys, **{d: np.array(vs) for d, vs in vals.items()})
cmapper = self._get_colormapper(cdim, element, ranges, style)
mapping['line_color'] = {'field': dim_name, 'transform': cmapper}
vals[dim_name] = cvals[:-1]
array = path.array(path.kdims)
alen = len(array)
paths = [array[s1:s2+1] for (s1, s2) in zip(range(alen-1), range(1, alen+1))]
if not hover:
continue
for vd in element.vdims:
values = path.dimension_values(vd)[:-1]
vd_name = util.dimension_sanitizer(vd.name)
vals[vd_name] = values
if values.dtype.kind == 'M':
vals[vd_name+'_dt_strings'] = [vd.pprint_value(v) for v in values]

xs, ys = ([path[:, idx] for path in paths] for idx in inds)
data = dict(xs=xs, ys=ys, **{d: np.asarray(vs) for d, vs in vals.items()})
self._get_hover_data(data, element)
return data, mapping, style

Expand Down
11 changes: 6 additions & 5 deletions holoviews/plotting/mpl/element.py
Expand Up @@ -15,7 +15,7 @@
from ...core import (OrderedDict, NdOverlay, DynamicMap, Dataset,
CompositeOverlay, Element3D, Element)
from ...core.options import abbreviated_exception
from ...element import Graph
from ...element import Graph, Path, Contours
from ...util.transform import dim
from ..plot import GenericElementPlot, GenericOverlayPlot
from ..util import dynamic_update, process_cmap, color_intervals, dim_range_key
Expand Down Expand Up @@ -77,7 +77,7 @@ class ElementPlot(GenericElementPlot, MPLPlot):
style_opts = []

# Declare which styles cannot be mapped to a non-scalar dimension
_nonvectorized_styles = ['marker', 'alpha', 'cmap', 'angle']
_nonvectorized_styles = ['marker', 'alpha', 'cmap', 'angle', 'visible']

# Whether plot has axes, disables setting axis limits, labels and ticks
_has_axes = True
Expand Down Expand Up @@ -538,6 +538,9 @@ def _apply_transforms(self, element, ranges, style):

if len(v.ops) == 0 and v.dimension in self.overlay_dims:
val = self.overlay_dims[v.dimension]
elif isinstance(element, Path) and not isinstance(element, Contours):
val = np.concatenate([v.apply(el, ranges=ranges, flat=True)[:-1]
for el in element.split()])
else:
val = v.apply(element, ranges)

Expand All @@ -554,9 +557,7 @@ def _apply_transforms(self, element, ranges, style):
'to the {style} use a groupby operation '
'to overlay your data along the dimension.'.format(
style=k, dim=v.dimension, element=element,
backend=self.renderer.backend
)
)
backend=self.renderer.backend))

style_groups = getattr(self, '_style_groups', [])
groups = [sg for sg in style_groups if k.startswith(sg)]
Expand Down
22 changes: 13 additions & 9 deletions holoviews/plotting/mpl/path.py
Expand Up @@ -37,22 +37,26 @@ def get_data(self, element, ranges, style):

cdim = element.get_dimension(self.color_index)
if cdim: cidx = element.get_dimension_index(cdim)
if not cdim:
style_mapping = any(True for v in style.values() if isinstance(v, np.ndarray))
if not (cdim or style_mapping):
paths = element.split(datatype='array', dimensions=element.kdims)
if self.invert_axes:
paths = [p[:, ::-1] for p in paths]
return (paths,), style, {}
paths, cvals = [], []
for path in element.split(datatype='array'):
splits = [0]+list(np.where(np.diff(path[:, cidx])!=0)[0]+1)
if len(splits) == 1:
splits.append(len(path))
for (s1, s2) in zip(splits[:-1], splits[1:]):
cvals.append(path[s1, cidx])
length = len(path)
for (s1, s2) in zip(range(length-1), range(1, length+1)):
if cdim:
cvals.append(path[s1, cidx])
paths.append(path[s1:s2+1, :2])
self._norm_kwargs(element, ranges, style, cdim)
style['array'] = np.array(cvals)
style['clim'] = style.pop('vmin', None), style.pop('vmax', None)
if cdim:
self._norm_kwargs(element, ranges, style, cdim)
style['array'] = np.array(cvals)
if 'c' in style:
style['array'] = style.pop('c')
if 'vmin' in style:
style['clim'] = style.pop('vmin', None), style.pop('vmax', None)
return (paths,), style, {}

def init_artists(self, ax, plot_args, plot_kwargs):
Expand Down
71 changes: 61 additions & 10 deletions holoviews/tests/plotting/bokeh/testpathplot.py
Expand Up @@ -71,7 +71,7 @@ def test_path_colored_and_split_with_extra_vdims(self):
color = [0, 0.25, 0.5, 0.75]
other = ['A', 'B', 'C', 'D']
data = {'x': xs, 'y': ys, 'color': color, 'other': other}
path = Path([data], vdims=['color','other']).options(color_index='color')
path = Path([data], vdims=['color','other']).options(color_index='color', tools=['hover'])
plot = bokeh_renderer.get_plot(path)
source = plot.handles['source']

Expand All @@ -89,9 +89,9 @@ def test_path_colored_and_split_on_single_value(self):
plot = bokeh_renderer.get_plot(path)
source = plot.handles['source']

self.assertEqual(source.data['xs'], [np.array([1, 2, 3, 4])])
self.assertEqual(source.data['ys'], [np.array([4, 3, 2, 1])])
self.assertEqual(source.data['color'], np.array([1]))
self.assertEqual(source.data['xs'], [np.array([1, 2]), np.array([2, 3]), np.array([3, 4])])
self.assertEqual(source.data['ys'], [np.array([4, 3]), np.array([3, 2]), np.array([2, 1])])
self.assertEqual(source.data['color'], np.array([1, 1, 1]))

def test_path_colored_by_levels_single_value(self):
xs = [1, 2, 3, 4]
Expand All @@ -101,20 +101,71 @@ def test_path_colored_by_levels_single_value(self):
data = {'x': xs, 'y': ys, 'color': color, 'date': date}
levels = [0, 38, 73, 95, 110, 130, 156, 999]
colors = ['#5ebaff', '#00faf4', '#ffffcc', '#ffe775', '#ffc140', '#ff8f20', '#ff6060']
path = Path([data], vdims=['color', 'date']).options(color_index='color', color_levels=levels, cmap=colors)
path = Path([data], vdims=['color', 'date']).options(
color_index='color', color_levels=levels, cmap=colors, tools=['hover'])
plot = bokeh_renderer.get_plot(path)
source = plot.handles['source']
cmapper = plot.handles['color_mapper']

self.assertEqual(source.data['xs'], [np.array([1, 2, 3, 4])])
self.assertEqual(source.data['ys'], [np.array([4, 3, 2, 1])])
self.assertEqual(source.data['color'], np.array([998]))
self.assertEqual(source.data['date'], np.array([1533081600000000000]))
self.assertEqual(source.data['date_dt_strings'], np.array(['2018-08-01 00:00:00']))
self.assertEqual(source.data['xs'], [np.array([1, 2]), np.array([2, 3]), np.array([3, 4])])
self.assertEqual(source.data['ys'], [np.array([4, 3]), np.array([3, 2]), np.array([2, 1])])
self.assertEqual(source.data['color'], np.array([998, 998, 998]))
self.assertEqual(source.data['date'],
np.array([1533081600000000000, 1533081600000000000, 1533081600000000000]))
self.assertEqual(source.data['date_dt_strings'],
np.array(['2018-08-01 00:00:00', '2018-08-01 00:00:00', '2018-08-01 00:00:00']))
self.assertEqual(cmapper.low, 156)
self.assertEqual(cmapper.high, 999)
self.assertEqual(cmapper.palette, colors[-1:])

def test_path_continuously_varying_color_op(self):
xs = [1, 2, 3, 4]
ys = xs[::-1]
color = [998, 999, 998, 994]
date = np.datetime64(dt.datetime(2018, 8, 1))
data = {'x': xs, 'y': ys, 'color': color, 'date': date}
levels = [0, 38, 73, 95, 110, 130, 156, 999]
colors = ['#5ebaff', '#00faf4', '#ffffcc', '#ffe775', '#ffc140', '#ff8f20', '#ff6060']
path = Path([data], vdims=['color', 'date']).options(
color='color', color_levels=levels, cmap=colors, tools=['hover'])
plot = bokeh_renderer.get_plot(path)
source = plot.handles['source']
cmapper = plot.handles['color_color_mapper']

self.assertEqual(source.data['xs'], [np.array([1, 2]), np.array([2, 3]), np.array([3, 4])])
self.assertEqual(source.data['ys'], [np.array([4, 3]), np.array([3, 2]), np.array([2, 1])])
self.assertEqual(source.data['color'], np.array([998, 999, 998]))
self.assertEqual(source.data['date'],
np.array([1533081600000000000, 1533081600000000000, 1533081600000000000]))
self.assertEqual(source.data['date_dt_strings'],
np.array(['2018-08-01 00:00:00', '2018-08-01 00:00:00', '2018-08-01 00:00:00']))
self.assertEqual(cmapper.low, 994)
self.assertEqual(cmapper.high, 999)
self.assertEqual(cmapper.palette, colors[-1:])

def test_path_continuously_varying_alpha_op(self):
xs = [1, 2, 3, 4]
ys = xs[::-1]
alpha = [0.1, 0.7, 0.3, 0.2]
data = {'x': xs, 'y': ys, 'alpha': alpha}
path = Path([data], vdims='alpha').options(alpha='alpha')
plot = bokeh_renderer.get_plot(path)
source = plot.handles['source']
self.assertEqual(source.data['xs'], [np.array([1, 2]), np.array([2, 3]), np.array([3, 4])])
self.assertEqual(source.data['ys'], [np.array([4, 3]), np.array([3, 2]), np.array([2, 1])])
self.assertEqual(source.data['alpha'], np.array([0.1, 0.7, 0.3]))

def test_path_continuously_varying_line_width_op(self):
xs = [1, 2, 3, 4]
ys = xs[::-1]
line_width = [1, 7, 3, 2]
data = {'x': xs, 'y': ys, 'line_width': line_width}
path = Path([data], vdims='line_width').options(line_width='line_width')
plot = bokeh_renderer.get_plot(path)
source = plot.handles['source']
self.assertEqual(source.data['xs'], [np.array([1, 2]), np.array([2, 3]), np.array([3, 4])])
self.assertEqual(source.data['ys'], [np.array([4, 3]), np.array([3, 2]), np.array([2, 1])])
self.assertEqual(source.data['line_width'], np.array([1, 7, 3]))


class TestPolygonPlot(TestBokehPlot):
Expand Down
51 changes: 50 additions & 1 deletion holoviews/tests/plotting/matplotlib/testpathplot.py
Expand Up @@ -2,11 +2,60 @@

from holoviews.core import NdOverlay
from holoviews.core.spaces import HoloMap
from holoviews.element import Polygons, Contours
from holoviews.element import Polygons, Contours, Path

from .testplot import TestMPLPlot, mpl_renderer


class TestPathPlot(TestMPLPlot):

def test_path_continuously_varying_color_op(self):
xs = [1, 2, 3, 4]
ys = xs[::-1]
color = [998, 999, 998, 994]
data = {'x': xs, 'y': ys, 'color': color}
levels = [0, 38, 73, 95, 110, 130, 156, 999]
colors = ['#5ebaff', '#00faf4', '#ffffcc', '#ffe775', '#ffc140', '#ff8f20', '#ff6060']
path = Path([data], vdims='color').options(
color='color', color_levels=levels, cmap=colors)
plot = mpl_renderer.get_plot(path)
artist = plot.handles['artist']
self.assertEqual(artist.get_array(), np.array([998, 999, 998]))
self.assertEqual(artist.get_clim(), (994, 999))

def test_path_continuously_varying_alpha_op(self):
xs = [1, 2, 3, 4]
ys = xs[::-1]
alpha = [0.1, 0.7, 0.3, 0.2]
data = {'x': xs, 'y': ys, 'alpha': alpha}
path = Path([data], vdims='alpha').options(alpha='alpha')
with self.assertRaises(Exception):
mpl_renderer.get_plot(path)

def test_path_continuously_varying_line_width_op(self):
xs = [1, 2, 3, 4]
ys = xs[::-1]
line_width = [1, 7, 3, 2]
data = {'x': xs, 'y': ys, 'line_width': line_width}
path = Path([data], vdims='line_width').options(linewidth='line_width')
plot = mpl_renderer.get_plot(path)
artist = plot.handles['artist']
self.assertEqual(artist.get_linewidths(), [1, 7, 3])

def test_path_continuously_varying_line_width_op_update(self):
xs = [1, 2, 3, 4]
ys = xs[::-1]
path = HoloMap({
0: Path([{'x': xs, 'y': ys, 'line_width': [1, 7, 3, 2]}], vdims='line_width'),
1: Path([{'x': xs, 'y': ys, 'line_width': [3, 8, 2, 3]}], vdims='line_width')
}).options(linewidth='line_width')
plot = mpl_renderer.get_plot(path)
artist = plot.handles['artist']
self.assertEqual(artist.get_linewidths(), [1, 7, 3])
plot.update((1,))
self.assertEqual(artist.get_linewidths(), [3, 8, 2])


class TestPolygonPlot(TestMPLPlot):

def test_polygons_colored(self):
Expand Down

0 comments on commit 5862f23

Please sign in to comment.