Skip to content

Commit 5862f23

Browse files
philippjfrjlstevens
authored andcommitted
Implemented continuous style mapping for Paths (#3192)
1 parent 87e7ba2 commit 5862f23

7 files changed

Lines changed: 191 additions & 68 deletions

File tree

holoviews/plotting/bokeh/element.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from ...core import DynamicMap, CompositeOverlay, Element, Dimension
2828
from ...core.options import abbreviated_exception, SkipRendering
2929
from ...core import util
30-
from ...element import Graph, VectorField
30+
from ...element import Graph, VectorField, Path, Contours
3131
from ...streams import Buffer
3232
from ...util.transform import dim
3333
from ..plot import GenericElementPlot, GenericOverlayPlot
@@ -680,6 +680,9 @@ def _apply_transforms(self, element, source, ranges, style, group=None):
680680

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

@@ -697,9 +700,7 @@ def _apply_transforms(self, element, source, ranges, style, group=None):
697700
'to the {style} use a groupby operation '
698701
'to overlay your data along the dimension.'.format(
699702
style=k, dim=v.dimension, element=element,
700-
backend=self.renderer.backend
701-
)
702-
)
703+
backend=self.renderer.backend))
703704
elif source.data and len(val) != len(list(source.data.values())[0]):
704705
if isinstance(element, VectorField):
705706
val = np.tile(val, 3)

holoviews/plotting/bokeh/path.py

Lines changed: 31 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,12 @@ def get_data(self, element, ranges, style):
6161
cdim = element.get_dimension(color)
6262
elif self.color_index is not None:
6363
cdim = element.get_dimension(self.color_index)
64+
style_mapping = any(
65+
s for s, v in style.items() if (s not in self._nonvectorized_styles) and
66+
(isinstance(v, util.basestring) and v in element) or isinstance(v, dim))
6467
inds = (1, 0) if self.invert_axes else (0, 1)
6568
mapping = dict(self._mapping)
66-
if not cdim:
69+
if not cdim and not style_mapping:
6770
if self.static_source:
6871
data = {}
6972
else:
@@ -72,32 +75,35 @@ def get_data(self, element, ranges, style):
7275
data = dict(xs=xs, ys=ys)
7376
return data, mapping, style
7477

75-
dim_name = util.dimension_sanitizer(cdim.name)
76-
if not self.static_source:
77-
paths = []
78+
vals = {}
79+
hover = 'hover' in self.handles
80+
if hover:
7881
vals = {util.dimension_sanitizer(vd.name): [] for vd in element.vdims}
79-
for path in element.split():
82+
if cdim:
83+
dim_name = util.dimension_sanitizer(cdim.name)
84+
cmapper = self._get_colormapper(cdim, element, ranges, style)
85+
mapping['line_color'] = {'field': dim_name, 'transform': cmapper}
86+
vals[dim_name] = []
87+
88+
paths = []
89+
for path in element.split():
90+
if cdim:
8091
cvals = path.dimension_values(cdim)
81-
array = path.array(path.kdims)
82-
splits = [0]+list(np.where(np.diff(cvals)!=0)[0]+1)
83-
cols = {vd.name: path.dimension_values(vd) for vd in element.vdims}
84-
if len(splits) == 1:
85-
splits.append(len(path))
86-
for (s1, s2) in zip(splits[:-1], splits[1:]):
87-
for i, vd in enumerate(element.vdims):
88-
path_val = cols[vd.name][s1]
89-
vd_column = util.dimension_sanitizer(vd.name)
90-
dt_column = vd_column+'_dt_strings'
91-
vals[vd_column].append(path_val)
92-
if isinstance(path_val, util.datetime_types):
93-
if dt_column not in vals:
94-
vals[dt_column] = []
95-
vals[dt_column].append(vd.pprint_value(path_val))
96-
paths.append(array[s1:s2+1])
97-
xs, ys = ([path[:, idx] for path in paths] for idx in inds)
98-
data = dict(xs=xs, ys=ys, **{d: np.array(vs) for d, vs in vals.items()})
99-
cmapper = self._get_colormapper(cdim, element, ranges, style)
100-
mapping['line_color'] = {'field': dim_name, 'transform': cmapper}
92+
vals[dim_name] = cvals[:-1]
93+
array = path.array(path.kdims)
94+
alen = len(array)
95+
paths = [array[s1:s2+1] for (s1, s2) in zip(range(alen-1), range(1, alen+1))]
96+
if not hover:
97+
continue
98+
for vd in element.vdims:
99+
values = path.dimension_values(vd)[:-1]
100+
vd_name = util.dimension_sanitizer(vd.name)
101+
vals[vd_name] = values
102+
if values.dtype.kind == 'M':
103+
vals[vd_name+'_dt_strings'] = [vd.pprint_value(v) for v in values]
104+
105+
xs, ys = ([path[:, idx] for path in paths] for idx in inds)
106+
data = dict(xs=xs, ys=ys, **{d: np.asarray(vs) for d, vs in vals.items()})
101107
self._get_hover_data(data, element)
102108
return data, mapping, style
103109

holoviews/plotting/mpl/element.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from ...core import (OrderedDict, NdOverlay, DynamicMap, Dataset,
1616
CompositeOverlay, Element3D, Element)
1717
from ...core.options import abbreviated_exception
18-
from ...element import Graph
18+
from ...element import Graph, Path, Contours
1919
from ...util.transform import dim
2020
from ..plot import GenericElementPlot, GenericOverlayPlot
2121
from ..util import dynamic_update, process_cmap, color_intervals, dim_range_key
@@ -77,7 +77,7 @@ class ElementPlot(GenericElementPlot, MPLPlot):
7777
style_opts = []
7878

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

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

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

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

561562
style_groups = getattr(self, '_style_groups', [])
562563
groups = [sg for sg in style_groups if k.startswith(sg)]

holoviews/plotting/mpl/path.py

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,26 @@ def get_data(self, element, ranges, style):
3737

3838
cdim = element.get_dimension(self.color_index)
3939
if cdim: cidx = element.get_dimension_index(cdim)
40-
if not cdim:
40+
style_mapping = any(True for v in style.values() if isinstance(v, np.ndarray))
41+
if not (cdim or style_mapping):
4142
paths = element.split(datatype='array', dimensions=element.kdims)
4243
if self.invert_axes:
4344
paths = [p[:, ::-1] for p in paths]
4445
return (paths,), style, {}
4546
paths, cvals = [], []
4647
for path in element.split(datatype='array'):
47-
splits = [0]+list(np.where(np.diff(path[:, cidx])!=0)[0]+1)
48-
if len(splits) == 1:
49-
splits.append(len(path))
50-
for (s1, s2) in zip(splits[:-1], splits[1:]):
51-
cvals.append(path[s1, cidx])
48+
length = len(path)
49+
for (s1, s2) in zip(range(length-1), range(1, length+1)):
50+
if cdim:
51+
cvals.append(path[s1, cidx])
5252
paths.append(path[s1:s2+1, :2])
53-
self._norm_kwargs(element, ranges, style, cdim)
54-
style['array'] = np.array(cvals)
55-
style['clim'] = style.pop('vmin', None), style.pop('vmax', None)
53+
if cdim:
54+
self._norm_kwargs(element, ranges, style, cdim)
55+
style['array'] = np.array(cvals)
56+
if 'c' in style:
57+
style['array'] = style.pop('c')
58+
if 'vmin' in style:
59+
style['clim'] = style.pop('vmin', None), style.pop('vmax', None)
5660
return (paths,), style, {}
5761

5862
def init_artists(self, ax, plot_args, plot_kwargs):

holoviews/tests/plotting/bokeh/testpathplot.py

Lines changed: 61 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def test_path_colored_and_split_with_extra_vdims(self):
7171
color = [0, 0.25, 0.5, 0.75]
7272
other = ['A', 'B', 'C', 'D']
7373
data = {'x': xs, 'y': ys, 'color': color, 'other': other}
74-
path = Path([data], vdims=['color','other']).options(color_index='color')
74+
path = Path([data], vdims=['color','other']).options(color_index='color', tools=['hover'])
7575
plot = bokeh_renderer.get_plot(path)
7676
source = plot.handles['source']
7777

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

92-
self.assertEqual(source.data['xs'], [np.array([1, 2, 3, 4])])
93-
self.assertEqual(source.data['ys'], [np.array([4, 3, 2, 1])])
94-
self.assertEqual(source.data['color'], np.array([1]))
92+
self.assertEqual(source.data['xs'], [np.array([1, 2]), np.array([2, 3]), np.array([3, 4])])
93+
self.assertEqual(source.data['ys'], [np.array([4, 3]), np.array([3, 2]), np.array([2, 1])])
94+
self.assertEqual(source.data['color'], np.array([1, 1, 1]))
9595

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

109-
self.assertEqual(source.data['xs'], [np.array([1, 2, 3, 4])])
110-
self.assertEqual(source.data['ys'], [np.array([4, 3, 2, 1])])
111-
self.assertEqual(source.data['color'], np.array([998]))
112-
self.assertEqual(source.data['date'], np.array([1533081600000000000]))
113-
self.assertEqual(source.data['date_dt_strings'], np.array(['2018-08-01 00:00:00']))
110+
self.assertEqual(source.data['xs'], [np.array([1, 2]), np.array([2, 3]), np.array([3, 4])])
111+
self.assertEqual(source.data['ys'], [np.array([4, 3]), np.array([3, 2]), np.array([2, 1])])
112+
self.assertEqual(source.data['color'], np.array([998, 998, 998]))
113+
self.assertEqual(source.data['date'],
114+
np.array([1533081600000000000, 1533081600000000000, 1533081600000000000]))
115+
self.assertEqual(source.data['date_dt_strings'],
116+
np.array(['2018-08-01 00:00:00', '2018-08-01 00:00:00', '2018-08-01 00:00:00']))
114117
self.assertEqual(cmapper.low, 156)
115118
self.assertEqual(cmapper.high, 999)
116119
self.assertEqual(cmapper.palette, colors[-1:])
117120

121+
def test_path_continuously_varying_color_op(self):
122+
xs = [1, 2, 3, 4]
123+
ys = xs[::-1]
124+
color = [998, 999, 998, 994]
125+
date = np.datetime64(dt.datetime(2018, 8, 1))
126+
data = {'x': xs, 'y': ys, 'color': color, 'date': date}
127+
levels = [0, 38, 73, 95, 110, 130, 156, 999]
128+
colors = ['#5ebaff', '#00faf4', '#ffffcc', '#ffe775', '#ffc140', '#ff8f20', '#ff6060']
129+
path = Path([data], vdims=['color', 'date']).options(
130+
color='color', color_levels=levels, cmap=colors, tools=['hover'])
131+
plot = bokeh_renderer.get_plot(path)
132+
source = plot.handles['source']
133+
cmapper = plot.handles['color_color_mapper']
134+
135+
self.assertEqual(source.data['xs'], [np.array([1, 2]), np.array([2, 3]), np.array([3, 4])])
136+
self.assertEqual(source.data['ys'], [np.array([4, 3]), np.array([3, 2]), np.array([2, 1])])
137+
self.assertEqual(source.data['color'], np.array([998, 999, 998]))
138+
self.assertEqual(source.data['date'],
139+
np.array([1533081600000000000, 1533081600000000000, 1533081600000000000]))
140+
self.assertEqual(source.data['date_dt_strings'],
141+
np.array(['2018-08-01 00:00:00', '2018-08-01 00:00:00', '2018-08-01 00:00:00']))
142+
self.assertEqual(cmapper.low, 994)
143+
self.assertEqual(cmapper.high, 999)
144+
self.assertEqual(cmapper.palette, colors[-1:])
145+
146+
def test_path_continuously_varying_alpha_op(self):
147+
xs = [1, 2, 3, 4]
148+
ys = xs[::-1]
149+
alpha = [0.1, 0.7, 0.3, 0.2]
150+
data = {'x': xs, 'y': ys, 'alpha': alpha}
151+
path = Path([data], vdims='alpha').options(alpha='alpha')
152+
plot = bokeh_renderer.get_plot(path)
153+
source = plot.handles['source']
154+
self.assertEqual(source.data['xs'], [np.array([1, 2]), np.array([2, 3]), np.array([3, 4])])
155+
self.assertEqual(source.data['ys'], [np.array([4, 3]), np.array([3, 2]), np.array([2, 1])])
156+
self.assertEqual(source.data['alpha'], np.array([0.1, 0.7, 0.3]))
157+
158+
def test_path_continuously_varying_line_width_op(self):
159+
xs = [1, 2, 3, 4]
160+
ys = xs[::-1]
161+
line_width = [1, 7, 3, 2]
162+
data = {'x': xs, 'y': ys, 'line_width': line_width}
163+
path = Path([data], vdims='line_width').options(line_width='line_width')
164+
plot = bokeh_renderer.get_plot(path)
165+
source = plot.handles['source']
166+
self.assertEqual(source.data['xs'], [np.array([1, 2]), np.array([2, 3]), np.array([3, 4])])
167+
self.assertEqual(source.data['ys'], [np.array([4, 3]), np.array([3, 2]), np.array([2, 1])])
168+
self.assertEqual(source.data['line_width'], np.array([1, 7, 3]))
118169

119170

120171
class TestPolygonPlot(TestBokehPlot):

holoviews/tests/plotting/matplotlib/testpathplot.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,60 @@
22

33
from holoviews.core import NdOverlay
44
from holoviews.core.spaces import HoloMap
5-
from holoviews.element import Polygons, Contours
5+
from holoviews.element import Polygons, Contours, Path
66

77
from .testplot import TestMPLPlot, mpl_renderer
88

99

10+
class TestPathPlot(TestMPLPlot):
11+
12+
def test_path_continuously_varying_color_op(self):
13+
xs = [1, 2, 3, 4]
14+
ys = xs[::-1]
15+
color = [998, 999, 998, 994]
16+
data = {'x': xs, 'y': ys, 'color': color}
17+
levels = [0, 38, 73, 95, 110, 130, 156, 999]
18+
colors = ['#5ebaff', '#00faf4', '#ffffcc', '#ffe775', '#ffc140', '#ff8f20', '#ff6060']
19+
path = Path([data], vdims='color').options(
20+
color='color', color_levels=levels, cmap=colors)
21+
plot = mpl_renderer.get_plot(path)
22+
artist = plot.handles['artist']
23+
self.assertEqual(artist.get_array(), np.array([998, 999, 998]))
24+
self.assertEqual(artist.get_clim(), (994, 999))
25+
26+
def test_path_continuously_varying_alpha_op(self):
27+
xs = [1, 2, 3, 4]
28+
ys = xs[::-1]
29+
alpha = [0.1, 0.7, 0.3, 0.2]
30+
data = {'x': xs, 'y': ys, 'alpha': alpha}
31+
path = Path([data], vdims='alpha').options(alpha='alpha')
32+
with self.assertRaises(Exception):
33+
mpl_renderer.get_plot(path)
34+
35+
def test_path_continuously_varying_line_width_op(self):
36+
xs = [1, 2, 3, 4]
37+
ys = xs[::-1]
38+
line_width = [1, 7, 3, 2]
39+
data = {'x': xs, 'y': ys, 'line_width': line_width}
40+
path = Path([data], vdims='line_width').options(linewidth='line_width')
41+
plot = mpl_renderer.get_plot(path)
42+
artist = plot.handles['artist']
43+
self.assertEqual(artist.get_linewidths(), [1, 7, 3])
44+
45+
def test_path_continuously_varying_line_width_op_update(self):
46+
xs = [1, 2, 3, 4]
47+
ys = xs[::-1]
48+
path = HoloMap({
49+
0: Path([{'x': xs, 'y': ys, 'line_width': [1, 7, 3, 2]}], vdims='line_width'),
50+
1: Path([{'x': xs, 'y': ys, 'line_width': [3, 8, 2, 3]}], vdims='line_width')
51+
}).options(linewidth='line_width')
52+
plot = mpl_renderer.get_plot(path)
53+
artist = plot.handles['artist']
54+
self.assertEqual(artist.get_linewidths(), [1, 7, 3])
55+
plot.update((1,))
56+
self.assertEqual(artist.get_linewidths(), [3, 8, 2])
57+
58+
1059
class TestPolygonPlot(TestMPLPlot):
1160

1261
def test_polygons_colored(self):

0 commit comments

Comments
 (0)