Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add responsive option for Plotly elements #4319

Merged
merged 1 commit into from
Mar 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions holoviews/plotting/plotly/element.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.""")

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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']

Expand Down
8 changes: 7 additions & 1 deletion holoviews/plotting/plotly/renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -70,13 +74,15 @@ 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)

# 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)
Expand Down
52 changes: 38 additions & 14 deletions holoviews/plotting/plotly/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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', {})
Expand Down Expand Up @@ -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])

Expand Down Expand Up @@ -730,27 +739,42 @@ 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)
)

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)
Expand Down