Skip to content

Commit eeb0ac4

Browse files
authored
Fix responsive resizing and cleanup div on plot removal in notebook (#1525)
- Cleanup div when plot is removed from notebook so that callbacks don't keep getting called and causing errors - Restore default figure height of 525 - Make iframe renderer responsive
1 parent 446e936 commit eeb0ac4

File tree

3 files changed

+118
-80
lines changed

3 files changed

+118
-80
lines changed

Diff for: plotly/io/_base_renderers.py

+58-19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from __future__ import absolute_import
12
import base64
23
import json
34
import webbrowser
@@ -238,8 +239,8 @@ def __init__(self,
238239
self.global_init = global_init
239240
self.requirejs = requirejs
240241
self.full_html = full_html
241-
self.post_script = post_script
242242
self.animation_opts = animation_opts
243+
self.post_script = post_script
243244

244245
def activate(self):
245246
if self.global_init:
@@ -310,15 +311,50 @@ def to_mimebundle(self, fig_dict):
310311
include_plotlyjs = True
311312
include_mathjax = 'cdn'
312313

314+
# build post script
315+
post_script = ["""
316+
var gd = document.getElementById('{plot_id}');
317+
var x = new MutationObserver(function (mutations, observer) {{
318+
var display = window.getComputedStyle(gd).display;
319+
if (!display || display === 'none') {{
320+
console.log([gd, 'removed!']);
321+
Plotly.purge(gd);
322+
observer.disconnect();
323+
}}
324+
}});
325+
326+
// Listen for the removal of the full notebook cells
327+
var notebookContainer = gd.closest('#notebook-container');
328+
if (notebookContainer) {{
329+
x.observe(notebookContainer, {childList: true});
330+
}}
331+
332+
// Listen for the clearing of the current output cell
333+
var outputEl = gd.closest('.output');
334+
if (outputEl) {{
335+
x.observe(outputEl, {childList: true});
336+
}}
337+
"""]
338+
339+
# Add user defined post script
340+
if self.post_script:
341+
if not isinstance(self.post_script, (list, tuple)):
342+
post_script.append(self.post_script)
343+
else:
344+
post_script.extend(self.post_script)
345+
313346
html = to_html(
314347
fig_dict,
315348
config=self.config,
316349
auto_play=self.auto_play,
317350
include_plotlyjs=include_plotlyjs,
318351
include_mathjax=include_mathjax,
319-
post_script=self.post_script,
352+
post_script=post_script,
320353
full_html=self.full_html,
321-
animation_opts=self.animation_opts)
354+
animation_opts=self.animation_opts,
355+
default_width='100%',
356+
default_height=525,
357+
)
322358

323359
return {'text/html': html}
324360

@@ -446,17 +482,16 @@ def to_mimebundle(self, fig_dict):
446482
# having iframe have its own scroll bar.
447483
iframe_buffer = 20
448484
layout = fig_dict.get('layout', {})
449-
if 'width' in layout:
450-
iframe_width = layout['width'] + iframe_buffer
485+
486+
if layout.get('width', False):
487+
iframe_width = str(layout['width'] + iframe_buffer) + 'px'
451488
else:
452-
layout['width'] = 700
453-
iframe_width = layout['width'] + iframe_buffer
489+
iframe_width = '100%'
454490

455-
if 'height' in layout:
491+
if layout.get('height', False):
456492
iframe_height = layout['height'] + iframe_buffer
457493
else:
458-
layout['height'] = 450
459-
iframe_height = layout['height'] + iframe_buffer
494+
iframe_height = str(525 + iframe_buffer) + 'px'
460495

461496
# Build filename using ipython cell number
462497
ip = IPython.get_ipython()
@@ -477,11 +512,14 @@ def to_mimebundle(self, fig_dict):
477512
auto_open=False,
478513
post_script=self.post_script,
479514
animation_opts=self.animation_opts,
515+
default_width='100%',
516+
default_height=525,
480517
validate=False)
481518

482519
# Build IFrame
483520
iframe_html = """\
484521
<iframe
522+
scrolling="no"
485523
width="{width}"
486524
height="{height}"
487525
src="{src}"
@@ -579,16 +617,17 @@ def __init__(self,
579617
self.animation_opts = animation_opts
580618

581619
def render(self, fig_dict):
582-
renderer = HtmlRenderer(
583-
connected=False,
584-
full_html=True,
585-
requirejs=False,
586-
global_init=False,
620+
from plotly.io import to_html
621+
html = to_html(
622+
fig_dict,
587623
config=self.config,
588624
auto_play=self.auto_play,
625+
include_plotlyjs=True,
626+
include_mathjax='cdn',
589627
post_script=self.post_script,
590-
animation_opts=self.animation_opts)
591-
592-
bundle = renderer.to_mimebundle(fig_dict)
593-
html = bundle['text/html']
628+
full_html=True,
629+
animation_opts=self.animation_opts,
630+
default_width='100%',
631+
default_height='100%',
632+
)
594633
open_html_in_browser(html, self.using, self.new, self.autoraise)

Diff for: plotly/io/_html.py

+60-18
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ def to_html(fig,
3131
post_script=None,
3232
full_html=True,
3333
animation_opts=None,
34+
default_width='100%',
35+
default_height='100%',
3436
validate=True):
3537
"""
3638
Convert a figure to an HTML string representation.
@@ -93,10 +95,10 @@ def to_html(fig,
9395
If a string that ends in '.js', a script tag is included that
9496
references the specified path. This approach can be used to point the
9597
resulting HTML div string to an alternative CDN.
96-
post_script: str or None (default None)
97-
JavaScript snippet to be included in the resulting div just after
98-
plot creation. The string may include '{plot_id}' placeholders that
99-
will then be replaced by the `id` of the div element that the
98+
post_script: str or list or None (default None)
99+
JavaScript snippet(s) to be included in the resulting div just after
100+
plot creation. The string(s) may include '{plot_id}' placeholders
101+
that will then be replaced by the `id` of the div element that the
100102
plotly.js figure is associated with. One application for this script
101103
is to install custom plotly.js event handlers.
102104
full_html: bool (default True)
@@ -109,6 +111,11 @@ def to_html(fig,
109111
https://github.com/plotly/plotly.js/blob/master/src/plots/animation_attributes.js
110112
for available options. Has no effect if the figure does not contain
111113
frames, or auto_play is False.
114+
default_width, default_height: number or str (default '100%')
115+
The default figure width/height to use if the provided figure does not
116+
specify its own layout.width/layout.height property. May be
117+
specified in pixels as an integer (e.g. 500), or as a css width style
118+
string (e.g. '500px', '100%').
112119
validate: bool (default True)
113120
True if the figure should be validated before being converted to
114121
JSON, False otherwise.
@@ -143,25 +150,47 @@ def to_html(fig,
143150
# ## Serialize figure config ##
144151
config = _get_jconfig(config)
145152

146-
# Check whether we should add responsive
147-
layout_dict = fig_dict.get('layout', {})
148-
if layout_dict.get('width', None) is None:
149-
config.setdefault('responsive', True)
153+
# Set responsive
154+
config.setdefault('responsive', True)
150155

151156
jconfig = json.dumps(config)
152157

158+
# Get div width/height
159+
layout_dict = fig_dict.get('layout', {})
160+
div_width = layout_dict.get('width', default_width)
161+
div_height = layout_dict.get('height', default_height)
162+
163+
# Add 'px' suffix to numeric widths
164+
try:
165+
float(div_width)
166+
except (ValueError, TypeError):
167+
pass
168+
else:
169+
div_width = str(div_width) + 'px'
170+
171+
try:
172+
float(div_height)
173+
except (ValueError, TypeError):
174+
pass
175+
else:
176+
div_height = str(div_height) + 'px'
177+
153178
# ## Get platform URL ##
154179
plotly_platform_url = config.get('plotlyServerURL', 'https://plot.ly')
155180

156181
# ## Build script body ##
157182
# This is the part that actually calls Plotly.js
183+
184+
# build post script snippet(s)
185+
then_post_script = ''
158186
if post_script:
159-
then_post_script = """.then(function(){{
187+
if not isinstance(post_script, (list, tuple)):
188+
post_script = [post_script]
189+
for ps in post_script:
190+
then_post_script += """.then(function(){{
160191
{post_script}
161192
}})""".format(
162-
post_script=post_script.replace('{plot_id}', plotdivid))
163-
else:
164-
then_post_script = ''
193+
post_script=ps.replace('{plot_id}', plotdivid))
165194

166195
then_addframes = ''
167196
then_animate = ''
@@ -274,7 +303,8 @@ def to_html(fig,
274303
<div>
275304
{mathjax_script}
276305
{load_plotlyjs}
277-
<div id="{id}" class="plotly-graph-div"></div>
306+
<div id="{id}" class="plotly-graph-div" \
307+
style="height:{height}; width:{width};"></div>
278308
<script type="text/javascript">
279309
{require_start}
280310
window.PLOTLYENV=window.PLOTLYENV || {{}};
@@ -286,6 +316,8 @@ def to_html(fig,
286316
mathjax_script=mathjax_script,
287317
load_plotlyjs=load_plotlyjs,
288318
id=plotdivid,
319+
width=div_width,
320+
height=div_height,
289321
plotly_platform_url=plotly_platform_url,
290322
require_start=require_start,
291323
script=script,
@@ -313,6 +345,8 @@ def write_html(fig,
313345
full_html=True,
314346
animation_opts=None,
315347
validate=True,
348+
default_width='100%',
349+
default_height='100%',
316350
auto_open=False):
317351
"""
318352
Write a figure to an HTML file representation
@@ -393,10 +427,10 @@ def write_html(fig,
393427
If a string that ends in '.js', a script tag is included that
394428
references the specified path. This approach can be used to point the
395429
resulting HTML div string to an alternative CDN.
396-
post_script: str or None (default None)
397-
JavaScript snippet to be included in the resulting div just after
398-
plot creation. The string may include '{plot_id}' placeholders that
399-
will then be replaced by the `id` of the div element that the
430+
post_script: str or list or None (default None)
431+
JavaScript snippet(s) to be included in the resulting div just after
432+
plot creation. The string(s) may include '{plot_id}' placeholders
433+
that will then be replaced by the `id` of the div element that the
400434
plotly.js figure is associated with. One application for this script
401435
is to install custom plotly.js event handlers.
402436
full_html: bool (default True)
@@ -409,6 +443,11 @@ def write_html(fig,
409443
https://github.com/plotly/plotly.js/blob/master/src/plots/animation_attributes.js
410444
for available options. Has no effect if the figure does not contain
411445
frames, or auto_play is False.
446+
default_width, default_height: number or str (default '100%')
447+
The default figure width/height to use if the provided figure does not
448+
specify its own layout.width/layout.height property. May be
449+
specified in pixels as an integer (e.g. 500), or as a css width style
450+
string (e.g. '500px', '100%').
412451
validate: bool (default True)
413452
True if the figure should be validated before being converted to
414453
JSON, False otherwise.
@@ -430,8 +469,11 @@ def write_html(fig,
430469
include_mathjax=include_mathjax,
431470
post_script=post_script,
432471
full_html=full_html,
472+
animation_opts=animation_opts,
473+
default_width=default_width,
474+
default_height=default_height,
433475
validate=validate,
434-
animation_opts=animation_opts)
476+
)
435477

436478
# Check if file is a string
437479
file_is_str = isinstance(file, six.string_types)

Diff for: plotly/tests/test_core/test_offline/test_offline.py

-43
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,6 @@
3535
]
3636
}
3737

38-
39-
resize_code_strings = ['"responsive": true']
40-
41-
4238
PLOTLYJS = plotly.offline.get_plotlyjs()
4339

4440
plotly_config_script = """\
@@ -266,45 +262,6 @@ def test_div_output(self):
266262
self.assertNotIn('</html>', html)
267263
self.assertTrue(html.startswith('<div>') and html.endswith('</div>'))
268264

269-
def test_autoresizing(self):
270-
271-
# If width or height wasn't specified, then we add a window resizer
272-
html = self._read_html(plotly.offline.plot(fig, auto_open=False))
273-
for resize_code_string in resize_code_strings:
274-
self.assertIn(resize_code_string, html)
275-
276-
# If width or height was specified, then we don't resize
277-
html = self._read_html(plotly.offline.plot({
278-
'data': fig['data'],
279-
'layout': {
280-
'width': 500, 'height': 500
281-
}
282-
}, auto_open=False))
283-
for resize_code_string in resize_code_strings:
284-
self.assertNotIn(resize_code_string, html)
285-
286-
def test_autoresizing_div(self):
287-
288-
# If width or height wasn't specified, then we add a window resizer
289-
for include_plotlyjs in [True, False, 'cdn', 'directory']:
290-
html = plotly.offline.plot(fig,
291-
output_type='div',
292-
include_plotlyjs=include_plotlyjs)
293-
294-
for resize_code_string in resize_code_strings:
295-
self.assertIn(resize_code_string, html)
296-
297-
# If width or height was specified, then we don't resize
298-
html = plotly.offline.plot({
299-
'data': fig['data'],
300-
'layout': {
301-
'width': 500, 'height': 500
302-
}
303-
}, output_type='div')
304-
305-
for resize_code_string in resize_code_strings:
306-
self.assertNotIn(resize_code_string, html)
307-
308265
def test_config(self):
309266
config = dict(linkText='Plotly rocks!',
310267
showLink=True,

0 commit comments

Comments
 (0)