Skip to content

Commit defab4c

Browse files
authored
offline.plot/iplot improvements (#1234)
* Support auto-resize in offline.plot with output_type='div' and include_plotlyjs=False * Support auto-resize for classic notebook iplot * Add 'cdn' and 'directory' include_plotlyjs options in offline.plot. When 'cdn', the resulting html file/div includes a script tag reference to the plotlyjs cdn. When 'directory', the resulting html file/div includes a script tag reference to a plotly.min.js bundle in the same directory as the html file. If output_type is 'file' then this plotly.min.js bundle is created in the output directory if it doesn't already exist. * Add option to specify include_plotlyjs as a path/url to a *.js file. This makes it possible to specify an alternative CDN or offline location for plotly.js
1 parent c62b66f commit defab4c

File tree

2 files changed

+259
-36
lines changed

2 files changed

+259
-36
lines changed

Diff for: plotly/offline/offline.py

+85-28
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import time
1313
import webbrowser
1414

15+
import six
1516
from requests.compat import json as _json
1617

1718
import plotly
@@ -358,8 +359,12 @@ def iplot(figure_or_data, show_link=True, link_text='Export to plot.ly',
358359
plot_html, plotdivid, width, height = _plot_html(
359360
figure_or_data, config, validate, '100%', 525, True
360361
)
361-
display_bundle['text/html'] = plot_html
362-
display_bundle['text/vnd.plotly.v1+html'] = plot_html
362+
resize_script = ''
363+
if width == '100%' or height == '100%':
364+
resize_script = _build_resize_script(plotdivid)
365+
366+
display_bundle['text/html'] = plot_html + resize_script
367+
display_bundle['text/vnd.plotly.v1+html'] = plot_html + resize_script
363368

364369
ipython_display.display(display_bundle, raw=True)
365370

@@ -389,6 +394,16 @@ def iplot(figure_or_data, show_link=True, link_text='Export to plot.ly',
389394
ipython_display.display(ipython_display.HTML(script))
390395

391396

397+
def _build_resize_script(plotdivid):
398+
resize_script = (
399+
'<script type="text/javascript">'
400+
'window.addEventListener("resize", function(){{'
401+
'Plotly.Plots.resize(document.getElementById("{id}"));}});'
402+
'</script>'
403+
).format(id=plotdivid)
404+
return resize_script
405+
406+
392407
def plot(figure_or_data, show_link=True, link_text='Export to plot.ly',
393408
validate=True, output_type='file', include_plotlyjs=True,
394409
filename='temp-plot.html', auto_open=True, image=None,
@@ -433,10 +448,41 @@ def plot(figure_or_data, show_link=True, link_text='Export to plot.ly',
433448
in a standalone HTML file.
434449
Use 'div' if you are embedding these graphs in an HTML file with
435450
other graphs or HTML markup, like a HTML report or an website.
436-
include_plotlyjs (default=True) -- If True, include the plotly.js
437-
source code in the output file or string.
438-
Set as False if your HTML file already contains a copy of the plotly.js
451+
include_plotlyjs (True | False | 'cdn' | 'directory' | path - default=True)
452+
Specifies how the plotly.js library is included in the output html
453+
file or div string.
454+
455+
If True, a script tag containing the plotly.js source code (~3MB)
456+
is included in the output. HTML files generated with this option are
457+
fully self-contained and can be used offline.
458+
459+
If 'cdn', a script tag that references the plotly.js CDN is included
460+
in the output. HTML files generated with this option are about 3MB
461+
smaller than those generated with include_plotlyjs=True, but they
462+
require an active internet connection in order to load the plotly.js
439463
library.
464+
465+
If 'directory', a script tag is included that references an external
466+
plotly.min.js bundle that is assumed to reside in the same
467+
directory as the HTML file. If output_type='file' then the
468+
plotly.min.js bundle is copied into the directory of the resulting
469+
HTML file. If a file named plotly.min.js already exists in the output
470+
directory then this file is left unmodified and no copy is performed.
471+
HTML files generated with this option can be used offline, but they
472+
require a copy of the plotly.min.js bundle in the same directory.
473+
This option is useful when many figures will be saved as HTML files in
474+
the same directory because the plotly.js source code will be included
475+
only once per output directory, rather than once per output file.
476+
477+
If a string that ends in '.js', a script tag is included that
478+
references the specified path. This approach can be used to point
479+
the resulting HTML file to an alternative CDN.
480+
481+
If False, no script tag referencing plotly.js is included. This is
482+
useful when output_type='div' and the resulting div string will be
483+
placed inside an HTML document that already loads plotly.js. This
484+
option is not advised when output_type='file' as it will result in
485+
a non-functional html file.
440486
filename (default='temp-plot.html') -- The local filename to save the
441487
outputted chart to. If the filename already exists, it will be
442488
overwritten. This argument only applies if `output_type` is 'file'.
@@ -477,25 +523,31 @@ def plot(figure_or_data, show_link=True, link_text='Export to plot.ly',
477523

478524
resize_script = ''
479525
if width == '100%' or height == '100%':
480-
resize_script = (
481-
''
482-
'<script type="text/javascript">'
483-
'window.addEventListener("resize", function(){{'
484-
'Plotly.Plots.resize(document.getElementById("{id}"));}});'
485-
'</script>'
486-
).format(id=plotdivid)
526+
resize_script = _build_resize_script(plotdivid)
527+
528+
if isinstance(include_plotlyjs, six.string_types):
529+
include_plotlyjs = include_plotlyjs.lower()
530+
531+
if include_plotlyjs == 'cdn':
532+
plotly_js_script = """\
533+
<script src="https://cdn.plot.ly/plotly-latest.min.js"></script>"""
534+
elif include_plotlyjs == 'directory':
535+
plotly_js_script = '<script src="plotly.min.js"></script>'
536+
elif (isinstance(include_plotlyjs, six.string_types) and
537+
include_plotlyjs.endswith('.js')):
538+
plotly_js_script = '<script src="{url}"></script>'.format(
539+
url=include_plotlyjs)
540+
elif include_plotlyjs:
541+
plotly_js_script = ''.join([
542+
'<script type="text/javascript">',
543+
get_plotlyjs(),
544+
'</script>',
545+
])
546+
else:
547+
plotly_js_script = ''
487548

488549
if output_type == 'file':
489550
with open(filename, 'w') as f:
490-
if include_plotlyjs:
491-
plotly_js_script = ''.join([
492-
'<script type="text/javascript">',
493-
get_plotlyjs(),
494-
'</script>',
495-
])
496-
else:
497-
plotly_js_script = ''
498-
499551
if image:
500552
if image not in __IMAGE_FORMATS:
501553
raise ValueError('The image parameter must be one of the '
@@ -523,25 +575,30 @@ def plot(figure_or_data, show_link=True, link_text='Export to plot.ly',
523575
'</body>',
524576
'</html>']))
525577

578+
# Check if we should copy plotly.min.js to output directory
579+
if include_plotlyjs == 'directory':
580+
bundle_path = os.path.join(
581+
os.path.dirname(filename), 'plotly.min.js')
582+
583+
if not os.path.exists(bundle_path):
584+
with open(bundle_path, 'w') as f:
585+
f.write(get_plotlyjs())
586+
526587
url = 'file://' + os.path.abspath(filename)
527588
if auto_open:
528589
webbrowser.open(url)
529590

530591
return url
531592

532593
elif output_type == 'div':
533-
if include_plotlyjs:
534-
return ''.join([
594+
595+
return ''.join([
535596
'<div>',
536-
'<script type="text/javascript">',
537-
get_plotlyjs(),
538-
'</script>',
597+
plotly_js_script,
539598
plot_html,
540599
resize_script,
541600
'</div>',
542601
])
543-
else:
544-
return plot_html
545602

546603

547604
def plot_mpl(mpl_fig, resize=False, strip_style=False,

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

+174-8
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,29 @@
2121
)
2222
}
2323

24+
25+
resize_code_strings = [
26+
'window.addEventListener("resize", ',
27+
'Plotly.Plots.resize('
28+
]
29+
30+
2431
PLOTLYJS = plotly.offline.offline.get_plotlyjs()
2532

33+
cdn_script = ('<script src="https://cdn.plot.ly/plotly-latest.min.js">'
34+
'</script>')
35+
36+
directory_script = '<script src="plotly.min.js"></script>'
37+
2638

2739
class PlotlyOfflineBaseTestCase(TestCase):
2840
def tearDown(self):
2941
# Some offline tests produce an html file. Make sure we clean up :)
3042
try:
3143
os.remove('temp-plot.html')
44+
# Some tests that produce temp-plot.html
45+
# also produce plotly.min.js
46+
os.remove('plotly.min.js')
3247
except OSError:
3348
pass
3449

@@ -64,10 +79,142 @@ def test_default_plot_generates_expected_html(self):
6479
# and it's an <html> doc
6580
self.assertTrue(html.startswith('<html>') and html.endswith('</html>'))
6681

67-
def test_including_plotlyjs(self):
68-
html = self._read_html(plotly.offline.plot(fig, include_plotlyjs=False,
69-
auto_open=False))
70-
self.assertNotIn(PLOTLYJS, html)
82+
def test_including_plotlyjs_truthy_html(self):
83+
# For backwards compatibility all truthy values that aren't otherwise
84+
# recognized are considered true
85+
for include_plotlyjs in [True, 34, 'non-empty-str']:
86+
html = self._read_html(plotly.offline.plot(
87+
fig,
88+
include_plotlyjs=include_plotlyjs,
89+
output_type='file',
90+
auto_open=False))
91+
self.assertIn(PLOTLYJS, html)
92+
self.assertNotIn(cdn_script, html)
93+
self.assertNotIn(directory_script, html)
94+
95+
def test_including_plotlyjs_truthy_div(self):
96+
# For backwards compatibility all truthy values that aren't otherwise
97+
# recognized are considered true
98+
for include_plotlyjs in [True, 34, 'non-empty-str']:
99+
html = plotly.offline.plot(
100+
fig,
101+
include_plotlyjs=include_plotlyjs,
102+
output_type='div')
103+
self.assertIn(PLOTLYJS, html)
104+
self.assertNotIn(cdn_script, html)
105+
self.assertNotIn(directory_script, html)
106+
107+
def test_including_plotlyjs_false_html(self):
108+
# For backwards compatibility all truthy values that aren't otherwise
109+
# recognized are considered true
110+
for include_plotlyjs in [False, 0, '']:
111+
html = self._read_html(plotly.offline.plot(
112+
fig,
113+
include_plotlyjs=include_plotlyjs,
114+
output_type='file',
115+
auto_open=False))
116+
self.assertNotIn(PLOTLYJS, html)
117+
self.assertNotIn(cdn_script, html)
118+
self.assertNotIn(directory_script, html)
119+
120+
def test_including_plotlyjs_false_div(self):
121+
for include_plotlyjs in [False, 0, '']:
122+
html = plotly.offline.plot(
123+
fig,
124+
include_plotlyjs=include_plotlyjs,
125+
output_type='div')
126+
self.assertNotIn(PLOTLYJS, html)
127+
self.assertNotIn(cdn_script, html)
128+
self.assertNotIn(directory_script, html)
129+
130+
def test_including_plotlyjs_cdn_html(self):
131+
for include_plotlyjs in ['cdn', 'CDN', 'Cdn']:
132+
html = self._read_html(plotly.offline.plot(
133+
fig,
134+
include_plotlyjs=include_plotlyjs,
135+
output_type='file',
136+
auto_open=False))
137+
self.assertNotIn(PLOTLYJS, html)
138+
self.assertIn(cdn_script, html)
139+
self.assertNotIn(directory_script, html)
140+
141+
def test_including_plotlyjs_cdn_div(self):
142+
for include_plotlyjs in ['cdn', 'CDN', 'Cdn']:
143+
html = plotly.offline.plot(
144+
fig,
145+
include_plotlyjs=include_plotlyjs,
146+
output_type='div')
147+
self.assertNotIn(PLOTLYJS, html)
148+
self.assertIn(cdn_script, html)
149+
self.assertNotIn(directory_script, html)
150+
151+
def test_including_plotlyjs_directory_html(self):
152+
self.assertFalse(os.path.exists('plotly.min.js'))
153+
154+
for include_plotlyjs in ['directory', 'Directory', 'DIRECTORY']:
155+
html = self._read_html(plotly.offline.plot(
156+
fig,
157+
include_plotlyjs=include_plotlyjs,
158+
auto_open=False))
159+
self.assertNotIn(PLOTLYJS, html)
160+
self.assertNotIn(cdn_script, html)
161+
self.assertIn(directory_script, html)
162+
163+
# plot creates plotly.min.js in the output directory
164+
self.assertTrue(os.path.exists('plotly.min.js'))
165+
with open('plotly.min.js', 'r') as f:
166+
self.assertEqual(f.read(), PLOTLYJS)
167+
168+
def test_including_plotlyjs_directory_div(self):
169+
self.assertFalse(os.path.exists('plotly.min.js'))
170+
171+
for include_plotlyjs in ['directory', 'Directory', 'DIRECTORY']:
172+
html = plotly.offline.plot(
173+
fig,
174+
include_plotlyjs=include_plotlyjs,
175+
output_type='div',
176+
auto_open=False)
177+
178+
self.assertNotIn(PLOTLYJS, html)
179+
self.assertNotIn(cdn_script, html)
180+
self.assertIn(directory_script, html)
181+
182+
# plot does NOT create a plotly.min.js file in the output directory
183+
# when output_type is div
184+
self.assertFalse(os.path.exists('plotly.min.js'))
185+
186+
def test_including_plotlyjs_path_html(self):
187+
for include_plotlyjs in [
188+
('https://cdnjs.cloudflare.com/ajax/libs/plotly.js/1.40.1/'
189+
'plotly.min.js'),
190+
'subpath/to/plotly.min.js',
191+
'something.js']:
192+
193+
html = self._read_html(plotly.offline.plot(
194+
fig,
195+
include_plotlyjs=include_plotlyjs,
196+
output_type='file',
197+
auto_open=False))
198+
self.assertNotIn(PLOTLYJS, html)
199+
self.assertNotIn(cdn_script, html)
200+
self.assertNotIn(directory_script, html)
201+
self.assertIn(include_plotlyjs, html)
202+
203+
def test_including_plotlyjs_path_div(self):
204+
for include_plotlyjs in [
205+
('https://cdnjs.cloudflare.com/ajax/libs/plotly.js/1.40.1/'
206+
'plotly.min.js'),
207+
'subpath/to/plotly.min.js',
208+
'something.js']:
209+
210+
html = plotly.offline.plot(
211+
fig,
212+
include_plotlyjs=include_plotlyjs,
213+
output_type='div')
214+
self.assertNotIn(PLOTLYJS, html)
215+
self.assertNotIn(cdn_script, html)
216+
self.assertNotIn(directory_script, html)
217+
self.assertIn(include_plotlyjs, html)
71218

72219
def test_div_output(self):
73220
html = plotly.offline.plot(fig, output_type='div', auto_open=False)
@@ -77,10 +224,7 @@ def test_div_output(self):
77224
self.assertTrue(html.startswith('<div>') and html.endswith('</div>'))
78225

79226
def test_autoresizing(self):
80-
resize_code_strings = [
81-
'window.addEventListener("resize", ',
82-
'Plotly.Plots.resize('
83-
]
227+
84228
# If width or height wasn't specified, then we add a window resizer
85229
html = self._read_html(plotly.offline.plot(fig, auto_open=False))
86230
for resize_code_string in resize_code_strings:
@@ -96,6 +240,28 @@ def test_autoresizing(self):
96240
for resize_code_string in resize_code_strings:
97241
self.assertNotIn(resize_code_string, html)
98242

243+
def test_autoresizing_div(self):
244+
245+
# If width or height wasn't specified, then we add a window resizer
246+
for include_plotlyjs in [True, False, 'cdn', 'directory']:
247+
html = plotly.offline.plot(fig,
248+
output_type='div',
249+
include_plotlyjs=include_plotlyjs)
250+
251+
for resize_code_string in resize_code_strings:
252+
self.assertIn(resize_code_string, html)
253+
254+
# If width or height was specified, then we don't resize
255+
html = plotly.offline.plot({
256+
'data': fig['data'],
257+
'layout': {
258+
'width': 500, 'height': 500
259+
}
260+
}, output_type='div')
261+
262+
for resize_code_string in resize_code_strings:
263+
self.assertNotIn(resize_code_string, html)
264+
99265
def test_config(self):
100266
config = dict(linkText='Plotly rocks!',
101267
editable=True)

0 commit comments

Comments
 (0)