diff --git a/holoviews/plotting/plotly/chart3d.py b/holoviews/plotting/plotly/chart3d.py index 2ee51e5497..31e4f73c53 100644 --- a/holoviews/plotting/plotly/chart3d.py +++ b/holoviews/plotting/plotly/chart3d.py @@ -46,7 +46,6 @@ class SurfacePlot(Chart3DPlot, ColorbarPlot): def graph_options(self, element, ranges, style): opts = super(SurfacePlot, self).graph_options(element, ranges, style) copts = self.get_color_opts(element.vdims[0], element, ranges, style) - copts['colorscale'] = style.get('cmap', 'Viridis') return dict(opts, **copts) def get_data(self, element, ranges, style): @@ -92,7 +91,7 @@ def get_data(self, element, ranges, style): points2D = np.vstack([x, y]).T tri = Delaunay(points2D) simplices = tri.simplices - return [dict(x=x, y=y, z=z, simplices=simplices, edges_color='black')] + return [dict(x=x, y=y, z=z, simplices=simplices)] def graph_options(self, element, ranges, style): opts = super(TriSurfacePlot, self).graph_options(element, ranges, style) @@ -101,8 +100,31 @@ def graph_options(self, element, ranges, style): for _, c in copts['colorscale']] opts['scale'] = [l for l, _ in copts['colorscale']] opts['show_colorbar'] = self.colorbar + edges_color = style.get('edges_color', None) + if edges_color: + opts['edges_color'] = edges_color + opts['plot_edges'] = True + + opts['colorbar'] = copts.get('colorbar', None) + return {k: v for k, v in opts.items() if 'legend' not in k and k != 'name'} def init_graph(self, data, options, index=0): - trace = super(TriSurfacePlot, self).init_graph(data, options, index) - return trisurface(**trace)[0].to_plotly_json() + trisurf_kwargs = super(TriSurfacePlot, self).init_graph( + data, options, index)[0] + + # Pop colorbar options since these aren't accepted by the trisurf + # figure factory. + colorbar = trisurf_kwargs.pop('colorbar', None) + trisurface_traces = trisurface(**trisurf_kwargs) + + # Find colorbar to set colorbar options. Colorbar is associated with + # a `scatter3d` scatter trace. + if colorbar: + marker_traces = [trace for trace in trisurface_traces + if trace.type == 'scatter3d' and + trace.mode == 'markers'] + if marker_traces: + marker_traces[0].marker.colorbar = colorbar + + return [t.to_plotly_json() for t in trisurface_traces] diff --git a/holoviews/plotting/plotly/element.py b/holoviews/plotting/plotly/element.py index bb809e30fa..9b5321f836 100644 --- a/holoviews/plotting/plotly/element.py +++ b/holoviews/plotting/plotly/element.py @@ -134,9 +134,9 @@ def generate_plot(self, key, ranges, element=None): opts = self.graph_options(element, ranges, style) graphs = [] for i, d in enumerate(data): - # Initialize graph - graph = self.init_graph(d, opts, index=i) - graphs.append(graph) + # Initialize traces + traces = self.init_graph(d, opts, index=i) + graphs.extend(traces) self.handles['graphs'] = graphs # Initialize layout @@ -186,7 +186,7 @@ def init_graph(self, data, options, index=0): trace[self._style_key] = dict(trace[self._style_key]) for s, val in vectorized.items(): trace[self._style_key][s] = val[index] - return trace + return [trace] def get_data(self, element, ranges, style): @@ -389,6 +389,7 @@ def get_color_opts(self, eldim, element, ranges, style): else: title = eldim.pprint_label opts['colorbar'] = dict(title=title, **self.colorbar_opts) + opts['showscale'] = True else: opts['showscale'] = False @@ -412,6 +413,21 @@ def get_color_opts(self, eldim, element, ranges, style): cmap = style.pop('cmap', 'viridis') colorscale = get_colorscale(cmap, self.color_levels, cmin, cmax) + + # Reduce colorscale length to <= 255 to work around + # https://github.com/plotly/plotly.js/issues/3699. Plotly.js performs + # colorscale interpolation internally so reducing the number of colors + # here makes very little difference to the displayed colorscale. + # + # Note that we need to be careful to make sure the first and last + # colorscale pairs, colorscale[0] and colorscale[-1], are preserved + # as the first and last in the subsampled colorscale + if isinstance(colorscale, list) and len(colorscale) > 255: + last_clr_pair = colorscale[-1] + step = int(np.ceil(len(colorscale) / 255)) + colorscale = colorscale[0::step] + colorscale[-1] = last_clr_pair + if cmin is not None: opts['cmin'] = cmin if cmax is not None: diff --git a/holoviews/plotting/plotly/stats.py b/holoviews/plotting/plotly/stats.py index 36bc0ae236..c8b55fb51d 100644 --- a/holoviews/plotting/plotly/stats.py +++ b/holoviews/plotting/plotly/stats.py @@ -9,7 +9,7 @@ class BivariatePlot(ChartPlot, ColorbarPlot): filled = param.Boolean(default=False) - + ncontours = param.Integer(default=None) trace_kwargs = {'type': 'histogram2dcontour'} @@ -20,10 +20,30 @@ class BivariatePlot(ChartPlot, ColorbarPlot): def graph_options(self, element, ranges, style): opts = super(BivariatePlot, self).graph_options(element, ranges, style) + copts = self.get_color_opts(element.vdims[0], element, ranges, style) + if self.ncontours: opts['autocontour'] = False opts['ncontours'] = self.ncontours - opts['contours'] = {'coloring': 'fill' if self.filled else 'lines'} + + # Make line width a little wider (default is less than 1) + opts['line'] = {'width': 1} + + # Configure contours + opts['contours'] = { + 'coloring': 'fill' if self.filled else 'lines', + 'showlines': style.get('showlines', True) + } + + # Add colorscale + opts['colorscale'] = copts['colorscale'] + + # Add colorbar + if 'colorbar' in copts: + opts['colorbar'] = copts['colorbar'] + + opts['showscale'] = copts.get('showscale', False) + return opts @@ -37,7 +57,7 @@ class DistributionPlot(ElementPlot): filled = param.Boolean(default=True, doc=""" Whether the bivariate contours should be filled.""") - + style_opts = ['color', 'dash', 'line_width'] trace_kwargs = {'type': 'scatter', 'mode': 'lines'} @@ -95,7 +115,7 @@ class BoxWhiskerPlot(MultiDistributionPlot): style_opts = ['color', 'alpha', 'outliercolor', 'marker', 'size'] trace_kwargs = {'type': 'box'} - + _style_key = 'marker' def graph_options(self, element, ranges, style): @@ -107,7 +127,7 @@ def graph_options(self, element, ranges, style): class ViolinPlot(MultiDistributionPlot): - + box = param.Boolean(default=True, doc=""" Whether to draw a boxplot inside the violin""") @@ -119,7 +139,7 @@ class ViolinPlot(MultiDistributionPlot): style_opts = ['color', 'alpha', 'outliercolor', 'marker', 'size'] trace_kwargs = {'type': 'violin'} - + _style_key = 'marker' def graph_options(self, element, ranges, style): diff --git a/holoviews/plotting/plotly/util.py b/holoviews/plotting/plotly/util.py index 5ba343e82d..5d4bd54a2a 100644 --- a/holoviews/plotting/plotly/util.py +++ b/holoviews/plotting/plotly/util.py @@ -654,9 +654,11 @@ def get_colorscale(cmap, levels=None, cmin=None, cmax=None): try: palette = process_cmap(cmap, ncolors) except Exception as e: - palette = colors.PLOTLY_SCALES.get(cmap) - if palette is None: + colorscale = colors.PLOTLY_SCALES.get(cmap) + if colorscale is None: raise e + return colorscale + if isinstance(levels, int): colorscale = [] scale = np.linspace(0, 1, levels+1) diff --git a/holoviews/tests/plotting/plotly/testbivariateplot.py b/holoviews/tests/plotting/plotly/testbivariateplot.py index f2e62dddca..2fc2ffdb50 100644 --- a/holoviews/tests/plotting/plotly/testbivariateplot.py +++ b/holoviews/tests/plotting/plotly/testbivariateplot.py @@ -22,9 +22,22 @@ def test_bivariate_filled(self): filled=True) state = self._get_plot_state(bivariate) self.assertEqual(state['data'][0]['contours']['coloring'], 'fill') - + def test_bivariate_ncontours(self): bivariate = Bivariate(([3, 2, 1], [0, 1, 2])).options(ncontours=5) state = self._get_plot_state(bivariate) self.assertEqual(state['data'][0]['ncontours'], 5) self.assertEqual(state['data'][0]['autocontour'], False) + + def test_bivariate_colorbar(self): + bivariate = Bivariate(([3, 2, 1], [0, 1, 2]))\ + + bivariate.opts(colorbar=True) + state = self._get_plot_state(bivariate) + trace = state['data'][0] + self.assertTrue(trace['showscale']) + + bivariate.opts(colorbar=False) + state = self._get_plot_state(bivariate) + trace = state['data'][0] + self.assertFalse(trace['showscale']) diff --git a/holoviews/tests/plotting/plotly/testsurfaceplot.py b/holoviews/tests/plotting/plotly/testsurfaceplot.py index 6d9a95212d..58914adcd6 100644 --- a/holoviews/tests/plotting/plotly/testsurfaceplot.py +++ b/holoviews/tests/plotting/plotly/testsurfaceplot.py @@ -19,3 +19,15 @@ def test_surface_state(self): self.assertEqual(state['layout']['scene']['xaxis']['range'], [0.5, 3.5]) self.assertEqual(state['layout']['scene']['yaxis']['range'], [-0.5, 1.5]) self.assertEqual(state['layout']['scene']['zaxis']['range'], [0, 4]) + + def test_surface_colorbar(self): + img = Surface(([1, 2, 3], [0, 1], np.array([[0, 1, 2], [2, 3, 4]]))) + img.opts(colorbar=True) + state = self._get_plot_state(img) + trace = state['data'][0] + self.assertTrue(trace['showscale']) + + img.opts(colorbar=False) + state = self._get_plot_state(img) + trace = state['data'][0] + self.assertFalse(trace['showscale'])