diff --git a/plotly/offline/offline.py b/plotly/offline/offline.py index 9d9c1e7394..031577c4d5 100644 --- a/plotly/offline/offline.py +++ b/plotly/offline/offline.py @@ -12,6 +12,7 @@ import time import webbrowser +import six from requests.compat import json as _json import plotly @@ -448,10 +449,37 @@ def plot(figure_or_data, show_link=True, link_text='Export to plot.ly', in a standalone HTML file. Use 'div' if you are embedding these graphs in an HTML file with other graphs or HTML markup, like a HTML report or an website. - include_plotlyjs (default=True) -- If True, include the plotly.js - source code in the output file or string. - Set as False if your HTML file already contains a copy of the plotly.js + include_plotlyjs (True | False | 'cdn' | 'directory' - default=True) -- + Specifies how the plotly.js library is included in the output html + file or div string. + + If True, a script tag containing the plotly.js source code (~3MB) + is included in the output. HTML files generated with this option are + fully self-contained and can be used offline. + + If 'cdn', a script tag that references the plotly.js CDN is included + in the output. HTML files generated with this option are about 3MB + smaller than those generated with include_plotlyjs=True, but they + require an active internet connection in order to load the plotly.js library. + + If 'directory', a script tag is included that references an external + plotly.min.js bundle that is assumed to reside in the same + directory as the HTML file. If output_type='file' then the + plotly.min.js bundle is copied into the directory of the resulting + HTML file. If a file named plotly.min.js already exists in the output + directory then this file is left unmodified and no copy is performed. + HTML files generated with this option can be used offline, but they + require a copy of the plotly.min.js bundle in the same directory. + This option is useful when many figures will be saved as HTML files in + the same directory because the plotly.js source code will be included + only once per output directory, rather than once per output file. + + If False, no script tag referencing plotly.js is included. This is + useful when output_type='div' and the resulting div string will be + placed inside an HTML document that already loads plotly.js. This + option is not advised when output_type='file' as it will result in + a non-functional html file. filename (default='temp-plot.html') -- The local filename to save the outputted chart to. If the filename already exists, it will be overwritten. This argument only applies if `output_type` is 'file'. @@ -494,17 +522,25 @@ def plot(figure_or_data, show_link=True, link_text='Export to plot.ly', if width == '100%' or height == '100%': resize_script = _build_resize_script(plotdivid) + if isinstance(include_plotlyjs, six.string_types): + include_plotlyjs = include_plotlyjs.lower() + + if include_plotlyjs == 'cdn': + plotly_js_script = """\ +""" + elif include_plotlyjs == 'directory': + plotly_js_script = '' + elif include_plotlyjs: + plotly_js_script = ''.join([ + '', + ]) + else: + plotly_js_script = '' + if output_type == 'file': with open(filename, 'w') as f: - if include_plotlyjs: - plotly_js_script = ''.join([ - '', - ]) - else: - plotly_js_script = '' - if image: if image not in __IMAGE_FORMATS: raise ValueError('The image parameter must be one of the ' @@ -532,6 +568,15 @@ def plot(figure_or_data, show_link=True, link_text='Export to plot.ly', '', ''])) + # Check if we should copy plotly.min.js to output directory + if include_plotlyjs == 'directory': + bundle_path = os.path.join( + os.path.dirname(filename), 'plotly.min.js') + + if not os.path.exists(bundle_path): + with open(bundle_path, 'w') as f: + f.write(get_plotlyjs()) + url = 'file://' + os.path.abspath(filename) if auto_open: webbrowser.open(url) @@ -539,19 +584,10 @@ def plot(figure_or_data, show_link=True, link_text='Export to plot.ly', return url elif output_type == 'div': - if include_plotlyjs: - return ''.join([ - '
', - '', - plot_html, - resize_script, - '
', - ]) - else: - return ''.join([ + + return ''.join([ '
', + plotly_js_script, plot_html, resize_script, '
', diff --git a/plotly/tests/test_core/test_offline/test_offline.py b/plotly/tests/test_core/test_offline/test_offline.py index 63184e34a3..429896a00b 100644 --- a/plotly/tests/test_core/test_offline/test_offline.py +++ b/plotly/tests/test_core/test_offline/test_offline.py @@ -30,12 +30,20 @@ PLOTLYJS = plotly.offline.offline.get_plotlyjs() +cdn_script = ('') + +directory_script = '' + class PlotlyOfflineBaseTestCase(TestCase): def tearDown(self): # Some offline tests produce an html file. Make sure we clean up :) try: os.remove('temp-plot.html') + # Some tests that produce temp-plot.html] + # also produce plotly.min.js + os.remove('plotly.min.js') except OSError: pass @@ -71,10 +79,109 @@ def test_default_plot_generates_expected_html(self): # and it's an doc self.assertTrue(html.startswith('') and html.endswith('')) - def test_including_plotlyjs(self): - html = self._read_html(plotly.offline.plot(fig, include_plotlyjs=False, - auto_open=False)) - self.assertNotIn(PLOTLYJS, html) + def test_including_plotlyjs_truthy_html(self): + # For backwards compatibility all truthy values that aren't otherwise + # recognized are considered true + for include_plotlyjs in [True, 34, 'non-empty-str']: + html = self._read_html(plotly.offline.plot( + fig, + include_plotlyjs=include_plotlyjs, + output_type='file', + auto_open=False)) + self.assertIn(PLOTLYJS, html) + self.assertNotIn(cdn_script, html) + self.assertNotIn(directory_script, html) + + def test_including_plotlyjs_truthy_div(self): + # For backwards compatibility all truthy values that aren't otherwise + # recognized are considered true + for include_plotlyjs in [True, 34, 'non-empty-str']: + html = plotly.offline.plot( + fig, + include_plotlyjs=include_plotlyjs, + output_type='div') + self.assertIn(PLOTLYJS, html) + self.assertNotIn(cdn_script, html) + self.assertNotIn(directory_script, html) + + def test_including_plotlyjs_false_html(self): + # For backwards compatibility all truthy values that aren't otherwise + # recognized are considered true + for include_plotlyjs in [False, 0, '']: + html = self._read_html(plotly.offline.plot( + fig, + include_plotlyjs=include_plotlyjs, + output_type='file', + auto_open=False)) + self.assertNotIn(PLOTLYJS, html) + self.assertNotIn(cdn_script, html) + self.assertNotIn(directory_script, html) + + def test_including_plotlyjs_false_div(self): + for include_plotlyjs in [False, 0, '']: + html = plotly.offline.plot( + fig, + include_plotlyjs=include_plotlyjs, + output_type='div') + self.assertNotIn(PLOTLYJS, html) + self.assertNotIn(cdn_script, html) + self.assertNotIn(directory_script, html) + + def test_including_plotlyjs_cdn_html(self): + for include_plotlyjs in ['cdn', 'CDN', 'Cdn']: + html = self._read_html(plotly.offline.plot( + fig, + include_plotlyjs=include_plotlyjs, + output_type='file', + auto_open=False)) + self.assertNotIn(PLOTLYJS, html) + self.assertIn(cdn_script, html) + self.assertNotIn(directory_script, html) + + def test_including_plotlyjs_cdn_div(self): + for include_plotlyjs in ['cdn', 'CDN', 'Cdn']: + html = plotly.offline.plot( + fig, + include_plotlyjs=include_plotlyjs, + output_type='div') + self.assertNotIn(PLOTLYJS, html) + self.assertIn(cdn_script, html) + self.assertNotIn(directory_script, html) + + def test_including_plotlyjs_directory_html(self): + self.assertFalse(os.path.exists('plotly.min.js')) + + for include_plotlyjs in ['directory', 'Directory', 'DIRECTORY']: + html = self._read_html(plotly.offline.plot( + fig, + include_plotlyjs=include_plotlyjs, + auto_open=False)) + self.assertNotIn(PLOTLYJS, html) + self.assertNotIn(cdn_script, html) + self.assertIn(directory_script, html) + + # plot creates plotly.min.js in the output directory + self.assertTrue(os.path.exists('plotly.min.js')) + with open('plotly.min.js', 'r') as f: + self.assertEqual(f.read(), PLOTLYJS) + + def test_including_plotlyjs_directory_div(self): + self.assertFalse(os.path.exists('plotly.min.js')) + + for include_plotlyjs in ['directory', 'Directory', 'DIRECTORY']: + html = plotly.offline.plot( + fig, + include_plotlyjs=include_plotlyjs, + output_type='div', + auto_open=False) + + self.assertNotIn(PLOTLYJS, html) + self.assertNotIn(cdn_script, html) + self.assertIn(directory_script, html) + + # plot does NOT create a plotly.min.js file in the output directory + # when output_type is div + self.assertFalse(os.path.exists('plotly.min.js')) def test_div_output(self): html = plotly.offline.plot(fig, output_type='div', auto_open=False) @@ -103,7 +210,7 @@ def test_autoresizing(self): def test_autoresizing_div(self): # If width or height wasn't specified, then we add a window resizer - for include_plotlyjs in [True, False]: + for include_plotlyjs in [True, False, 'cdn', 'directory']: html = plotly.offline.plot(fig, output_type='div', include_plotlyjs=include_plotlyjs)