diff --git a/holoviews/plotting/plotly/element.py b/holoviews/plotting/plotly/element.py index 3300a3ed7b..970983a22f 100644 --- a/holoviews/plotting/plotly/element.py +++ b/holoviews/plotting/plotly/element.py @@ -60,6 +60,9 @@ class ElementPlot(PlotlyPlot, GenericElementPlot): Margins in pixel values specified as a tuple of the form (left, bottom, right, top).""") + responsive = param.Boolean(default=False, doc=""" + Whether the plot should stretch to fill the available space.""") + show_legend = param.Boolean(default=False, doc=""" Whether to show legend for the plot.""") @@ -184,7 +187,8 @@ def generate_plot(self, key, ranges, element=None): self.handles['layout'] = layout # Create figure and return it - fig = dict(data=components['traces'], layout=layout) + layout['autosize'] = self.responsive + fig = dict(data=components['traces'], layout=layout, config=dict(responsive=self.responsive)) self.handles['fig'] = fig self._execute_hooks(element) @@ -472,8 +476,11 @@ def init_layout(self, key, element, ranges): options['yaxis'] = yaxis options['margin'] = dict(l=l, r=r, b=b, t=t, pad=4) - return dict(width=self.width, height=self.height, - title=self._format_title(key, separator=' '), + if not self.responsive: + options['width'] = self.width + options['height'] = self.height + + return dict(title=self._format_title(key, separator=' '), plot_bgcolor=self.bgcolor, **options) def _get_ticks(self, axis, ticker): @@ -580,7 +587,7 @@ class OverlayPlot(GenericOverlayPlot, ElementPlot): _propagate_options = [ 'width', 'height', 'xaxis', 'yaxis', 'labelled', 'bgcolor', 'invert_axes', 'show_frame', 'show_grid', 'logx', 'logy', - 'xticks', 'toolbar', 'yticks', 'xrotation', 'yrotation', + 'xticks', 'toolbar', 'yticks', 'xrotation', 'yrotation', 'responsive', 'invert_xaxis', 'invert_yaxis', 'sizing_mode', 'title', 'title_format', 'padding', 'xlabel', 'ylabel', 'zlabel', 'xlim', 'ylim', 'zlim'] diff --git a/holoviews/plotting/plotly/renderer.py b/holoviews/plotting/plotly/renderer.py index 06c91a6480..505b632199 100644 --- a/holoviews/plotting/plotly/renderer.py +++ b/holoviews/plotting/plotly/renderer.py @@ -28,7 +28,11 @@ def _PlotlyHoloviewsPane(fig_dict, **kwargs): # Remove internal HoloViews properties clean_internal_figure_properties(fig_dict) - plotly_pane = pn.pane.Plotly(fig_dict, viewport_update_policy='mouseup', **kwargs) + config = fig_dict.pop('config', {}) + if config.get('responsive'): + kwargs['sizing_mode'] = 'stretch_both' + plotly_pane = pn.pane.Plotly(fig_dict, viewport_update_policy='mouseup', + config=config, **kwargs) # Register callbacks on pane for callback_cls in callbacks.values(): @@ -70,6 +74,7 @@ def get_plot_state(self_or_cls, obj, doc=None, renderer=None, **kwargs): Allows cleaning the dictionary of any internal properties that were added """ fig_dict = super(PlotlyRenderer, self_or_cls).get_plot_state(obj, renderer, **kwargs) + config = fig_dict.get('config', {}) # Remove internal properties (e.g. '_id', '_dim') clean_internal_figure_properties(fig_dict) @@ -77,6 +82,7 @@ def get_plot_state(self_or_cls, obj, doc=None, renderer=None, **kwargs): # Run through Figure constructor to normalize keys # (e.g. to expand magic underscore notation) fig_dict = go.Figure(fig_dict).to_dict() + fig_dict['config'] = config # Remove template fig_dict.get('layout', {}).pop('template', None) diff --git a/holoviews/plotting/plotly/util.py b/holoviews/plotting/plotly/util.py index 747167f8bd..b425a061ea 100644 --- a/holoviews/plotting/plotly/util.py +++ b/holoviews/plotting/plotly/util.py @@ -450,12 +450,12 @@ def _scale_translate(fig, scale_x, scale_y, translate_x, translate_y): layout = fig.setdefault('layout', {}) def scale_translate_x(x): - return [x[0] * scale_x + translate_x, - x[1] * scale_x + translate_x] + return [min(x[0] * scale_x + translate_x, 1), + min(x[1] * scale_x + translate_x, 1)] def scale_translate_y(y): - return [y[0] * scale_y + translate_y, - y[1] * scale_y + translate_y] + return [min(y[0] * scale_y + translate_y, 1), + min(y[1] * scale_y + translate_y, 1)] def perform_scale_translate(obj): domain = obj.setdefault('domain', {}) @@ -673,17 +673,26 @@ def figure_grid(figures_grid, nrows = len(row_heights) ncols = len(column_widths) + responsive = True for r in range(nrows): for c in range(ncols): fig_element = figures_grid[r][c] if not fig_element: continue + responsive &= fig_element.get('config', {}).get('responsive', False) - w = fig_element.get('layout', {}).get('width', None) + default = None if responsive else 400 + for r in range(nrows): + for c in range(ncols): + fig_element = figures_grid[r][c] + if not fig_element: + continue + + w = fig_element.get('layout', {}).get('width', default) if w: column_widths[c] = max(w, column_widths[c]) - h = fig_element.get('layout', {}).get('height', None) + h = fig_element.get('layout', {}).get('height', default) if h: row_heights[r] = max(h, row_heights[r]) @@ -730,20 +739,33 @@ def figure_grid(figures_grid, _offset_subplot_ids(fig, subplot_offsets) - fig_height = fig['layout']['height'] * row_height_scale - fig_width = fig['layout']['width'] * column_width_scale - - scale_x = (column_domain[1] - column_domain[0]) * (fig_width / column_widths[c]) - scale_y = (row_domain[1] - row_domain[0]) * (fig_height / row_heights[r]) - _scale_translate(fig, - scale_x, scale_y, - column_domain[0], row_domain[0]) + if responsive: + scale_x = 1./ncols + scale_y = 1./nrows + px = ((0.2/(ncols) if ncols > 1 else 0)) + py = ((0.2/(nrows) if nrows > 1 else 0)) + sx = scale_x-px + sy = scale_y-py + _scale_translate(fig, sx, sy, scale_x*c+px/2., scale_y*r+py/2.) + else: + fig_height = fig['layout'].get('height', default) * row_height_scale + fig_width = fig['layout'].get('width', default) * column_width_scale + scale_x = (column_domain[1] - column_domain[0]) * (fig_width / column_widths[c]) + scale_y = (row_domain[1] - row_domain[0]) * (fig_height / row_heights[r]) + _scale_translate( + fig, scale_x, scale_y, column_domain[0], row_domain[0] + ) merge_figure(output_figure, fig) + if responsive: + output_figure['config'] = {'responsive': True} + # Set output figure width/height if height: output_figure['layout']['height'] = height + elif responsive: + output_figure['layout']['autosize'] = True else: output_figure['layout']['height'] = ( sum(row_heights) + row_spacing * (nrows - 1) @@ -751,6 +773,8 @@ def figure_grid(figures_grid, if width: output_figure['layout']['width'] = width + elif responsive: + output_figure['layout']['autosize'] = True else: output_figure['layout']['width'] = ( sum(column_widths) + column_spacing * (ncols - 1)