From 54c96fafe07e82643d11a086afc622828c5c9821 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 16 Mar 2019 14:34:13 -0400 Subject: [PATCH 01/48] Initial plotly.io.renderers implementation --- plotly/io/__init__.py | 2 + plotly/io/_renderers.py | 570 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 572 insertions(+) create mode 100644 plotly/io/_renderers.py diff --git a/plotly/io/__init__.py b/plotly/io/__init__.py index a7d9dacd5f4..6b395047200 100644 --- a/plotly/io/__init__.py +++ b/plotly/io/__init__.py @@ -4,3 +4,5 @@ from ._json import to_json, from_json, read_json, write_json from ._templates import templates, to_templated + +from ._renderers import renderers, show diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py new file mode 100644 index 00000000000..b63d8459161 --- /dev/null +++ b/plotly/io/_renderers.py @@ -0,0 +1,570 @@ +from __future__ import absolute_import, division + +import base64 +import json +import textwrap +import uuid +import six +import os + +from IPython.display import display_html, display + +from plotly import utils +from plotly.io import to_json, to_image +from plotly.io._orca import ensure_server +from plotly.offline.offline import _get_jconfig, get_plotlyjs +from plotly.io._utils import validate_coerce_fig_to_dict + + +# Renderer configuration class +# ----------------------------- +class RenderersConfig(object): + """ + Singleton object containing the current renderer configurations + """ + def __init__(self): + self._renderers = {} + self._default_name = None + self._default_renderers = [] + + # ### Magic methods ### + # Make this act as a dict of renderers + def __len__(self): + return len(self._renderers) + + def __contains__(self, item): + return item in self._renderers + + def __iter__(self): + return iter(self._renderers) + + def __getitem__(self, item): + renderer = self._renderers[item] + return renderer + + def __setitem__(self, key, value): + if not isinstance(value, MimetypeRenderer): + raise ValueError( + 'Renderer must be a subclass of MimetypeRenderer') + self._renderers[key] = value + + def __delitem__(self, key): + # Remove template + del self._renderers[key] + + # Check if we need to remove it as the default + if self._default == key: + self._default = None + + def keys(self): + return self._renderers.keys() + + def items(self): + return self._renderers.items() + + def update(self, d={}, **kwargs): + """ + Update one or more renderers from a dict or from input keyword + arguments. + + Parameters + ---------- + d: dict + Dictionary from renderer names to new renderer objects. + + kwargs + Named argument value pairs where the name is a renderer name + and the value is a new renderer object + """ + for k, v in dict(d, **kwargs).items(): + self[k] = v + + # ### Properties ### + @property + def default(self): + """ + The default renderer, or None if no there is no default + + If not None, the default renderer is automatically used to render + figures when they are displayed in a jupyter notebook or when using + the plotly.io.show function + + The names of available templates may be retrieved with: + + >>> import plotly.io as pio + >>> list(pio.renderers) + + Returns + ------- + str + """ + return self._default_name + + @default.setter + def default(self, value): + # Handle None + if value is None: + self._default_name = None + self._default_renderers = [] + return + + # Store defaults name and list of renderer(s) + renderer_names = self._validate_coerce_renderers(value) + self._default_name = value + self._default_renderers = [self[name] for name in renderer_names] + + # Activate default renderer(s) + for renderer in self._default_renderers: + renderer.activate() + + def _validate_coerce_renderers(self, renderers_string): + # Validate value + if not isinstance(renderers_string, six.string_types): + raise ValueError('Renderer must be specified as a string') + + renderer_names = renderers_string.split('+') + invalid = [name for name in renderer_names if name not in self] + if invalid: + raise ValueError(""" +Invalid named renderer(s) received: {}""".format(str(invalid))) + + return renderer_names + + + def __repr__(self): + return """\ +Renderers configuration +----------------------- + Default renderer: {default} + Available renderers: +{available} +""".format(default=repr(self.default), + available=self._available_templates_str()) + + def _available_templates_str(self): + """ + Return nicely wrapped string representation of all + available template names + """ + available = '\n'.join(textwrap.wrap( + repr(list(self)), + width=79 - 8, + initial_indent=' ' * 8, + subsequent_indent=' ' * 9 + )) + return available + + def _build_mime_bundle(self, fig_dict, renderers_string=None): + mime_renderers = True + assert mime_renderers + + if renderers_string: + renderer_names = self._validate_coerce_renderers(renderers_string) + renderers_list = [self[name] for name in renderer_names] + for renderer in renderers_list: + renderer.activate() + else: + renderers_list = self._default_renderers + + bundle = {} + for renderer in renderers_list: + bundle.update(renderer.to_mimebundle(fig_dict)) + + return bundle + + +# Make config a singleton object +# ------------------------------ +renderers = RenderersConfig() +del RenderersConfig + + +class MimetypeRenderer(object): + + def activate(self): + pass + + def to_mimebundle(self, fig_dict): + raise NotImplementedError() + + +# JSON +class JsonRenderer(MimetypeRenderer): + def to_mimebundle(self, fig_dict): + value = json.loads(to_json(fig_dict)) + return {'application/json': value} + + +# Plotly mimetype +class PlotlyRenderer(MimetypeRenderer): + def __init__(self, config=None): + self.config = dict(config) if config else {} + + def to_mimebundle(self, fig_dict): + config = _get_jconfig(self.config) + if config: + fig_dict['config'] = config + return {'application/vnd.plotly.v1+json': fig_dict} + + +# Static Image +class ImageRenderer(MimetypeRenderer): + def __init__(self, + mime_type, + b64_encode=False, + format=None, + width=None, + height=None, + scale=None): + + self.mime_type = mime_type + self.b64_encode = b64_encode + self.format = format + self.width = width + self.height = height + self.scale = scale + + def activate(self): + ensure_server() + + def to_mimebundle(self, fig_dict): + image_bytes = to_image( + fig_dict, + format=self.format, + width=self.width, + height=self.height, + scale=self.scale) + + if self.b64_encode: + image_str = base64.b64encode(image_bytes).decode('utf8') + else: + image_str = image_bytes.decode('utf8') + + return {self.mime_type: image_str} + + +class PngRenderer(ImageRenderer): + def __init__(self, width=None, height=None, scale=None): + super(PngRenderer, self).__init__( + mime_type='image/png', + b64_encode=True, + format='png', + width=width, + height=height, + scale=scale) + + +class SvgRenderer(ImageRenderer): + def __init__(self, width=None, height=None, scale=None): + super(SvgRenderer, self).__init__( + mime_type='image/svg+xml', + b64_encode=False, + format='svg', + width=width, + height=height, + scale=scale) + + +class PdfRenderer(ImageRenderer): + def __init__(self, width=None, height=None, scale=None): + super(PdfRenderer, self).__init__( + mime_type='application/pdf', + b64_encode=True, + format='pdf', + width=width, + height=height, + scale=scale) + + +class JpegRenderer(ImageRenderer): + def __init__(self, width=None, height=None, scale=None): + super(JpegRenderer, self).__init__( + mime_type='image/jpeg', + b64_encode=True, + format='jpg', + width=width, + height=height, + scale=scale) + + +# HTML +# Build script to set global PlotlyConfig object. This must execute before +# plotly.js is loaded. +_window_plotly_config = """\ +window.PlotlyConfig = {MathJaxConfig: 'local'};""" + + +class HtmlRenderer(MimetypeRenderer): + + def __init__(self, + connected=False, + fullhtml=False, + requirejs=True, + global_init=False, + config=None, + auto_play=False): + + self.config = dict(config) if config else {} + self.auto_play = auto_play + self.connected = connected + self.global_init = global_init + self.requirejs = requirejs + self.fullhtml = fullhtml + + def activate(self): + if self.global_init: + if not self.requirejs: + raise ValueError( + 'global_init is only supported with requirejs=True') + + if self.connected: + # Connected so we configure requirejs with the plotly CDN + script = """\ + + """.format(win_config=_window_plotly_config) + + else: + # If not connected then we embed a copy of the plotly.js + # library in the notebook + script = """\ + + """.format(script=get_plotlyjs(), win_config=_window_plotly_config) + + display_html(script, raw=True) + + def to_mimebundle(self, fig_dict): + plotdivid = uuid.uuid4() + + # Serialize figure + jdata = json.dumps( + fig_dict.get('data', []), cls=utils.PlotlyJSONEncoder) + jlayout = json.dumps( + fig_dict.get('layout', {}), cls=utils.PlotlyJSONEncoder) + + if fig_dict.get('frames', None): + jframes = json.dumps( + fig_dict.get('frames', []), cls=utils.PlotlyJSONEncoder) + else: + jframes = None + + # Serialize figure config + config = _get_jconfig(self.config) + jconfig = json.dumps(config) + + # Platform URL + plotly_platform_url = config.get('plotly_domain', 'https://plot.ly') + + # Build script body + if jframes: + if self.auto_play: + animate = """.then(function(){{ + Plotly.animate('{id}'); + }}""".format(id=plotdivid) + + else: + animate = '' + + script = ''' + if (document.getElementById("{id}")) {{ + Plotly.plot( + '{id}', + {data}, + {layout}, + {config} + ).then(function () {add_frames}){animate} + }} + '''.format( + id=plotdivid, + data=jdata, + layout=jlayout, + config=jconfig, + add_frames="{" + "return Plotly.addFrames('{id}',{frames}".format( + id=plotdivid, frames=jframes + ) + ");}", + animate=animate + ) + else: + script = """ + if (document.getElementById("{id}")) {{ + Plotly.newPlot("{id}", {data}, {layout}, {config}); + }} + """.format( + id=plotdivid, + data=jdata, + layout=jlayout, + config=jconfig) + + # Build div + + # Handle require + if self.requirejs: + require_start = 'require(["plotly"], function(Plotly) {' + require_end = '});' + load_plotlyjs = '' + else: + require_start = '' + require_end = '' + if self.connected: + load_plotlyjs = """\ + +""".format( + win_config=_window_plotly_config) + else: + load_plotlyjs = """\ + +""".format(win_config=_window_plotly_config, plotlyjs=get_plotlyjs()) + + # Handle fullhtml + if self.fullhtml: + html_start = '' + html_end = '' + else: + html_start = '' + html_end = '' + + plotly_html_div = """ +{html_start} + {load_plotlyjs} +
+ +{html_end}""".format( + html_start=html_start, + load_plotlyjs=load_plotlyjs, + id=plotdivid, + plotly_platform_url=plotly_platform_url, + require_start=require_start, + script=script, + require_end=require_end, + html_end=html_end) + + return {'text/html': plotly_html_div} + + +class NotebookRenderer(HtmlRenderer): + def __init__(self, connected=False, config=None, auto_play=False): + super(NotebookRenderer, self).__init__( + connected=connected, + fullhtml=False, + requirejs=True, + global_init=True, + config=config, + auto_play=auto_play) + + +class KaggleRenderer(HtmlRenderer): + """ + Same as NotebookRenderer but with connected True + """ + def __init__(self, config=None, auto_play=False): + super(KaggleRenderer, self).__init__( + connected=True, + fullhtml=False, + requirejs=True, + global_init=True, + config=config, + auto_play=auto_play) + + +class ColabRenderer(HtmlRenderer): + """ + Google Colab compatible HTML renderer + """ + def __init__(self, config=None, auto_play=False): + super(ColabRenderer, self).__init__( + connected=True, + fullhtml=True, + requirejs=False, + global_init=False, + config=config, + auto_play=auto_play) + + +# Show +def show(fig, renderer=None, validate=True): + fig_dict = validate_coerce_fig_to_dict(fig, validate) + + mime_renderers = True + if mime_renderers: + bundle = renderers._build_mime_bundle( + fig_dict, renderers_string=renderer) + display(bundle, raw=True) + + +# Register renderers + +# JSON +renderers['json'] = JsonRenderer() + +# Plotly mime type +plotly_renderer = PlotlyRenderer() +renderers['default'] = plotly_renderer +renderers['jupyterlab'] = plotly_renderer +renderers['nteract'] = plotly_renderer +renderers['vscode'] = plotly_renderer + +# HTML-based +config = {'responsive': True} +renderers['notebook'] = NotebookRenderer(config=config) +renderers['notebook_connected'] = NotebookRenderer( + config=config, connected=True) +renderers['kaggle'] = KaggleRenderer(config=config) +renderers['colab'] = ColabRenderer(config=config) + +# Static Image +img_kwargs = dict(height=450, width=700) +renderers['png'] = PngRenderer(**img_kwargs) +jpeg_renderer = JpegRenderer(**img_kwargs) +renderers['jpeg'] = jpeg_renderer +renderers['jpg'] = jpeg_renderer + +renderers['svg'] = SvgRenderer(**img_kwargs) +renderers['pdf'] = PdfRenderer(**img_kwargs) + +# Set default renderer +default_renderer = 'default' + +# Check Colab +try: + import google.colab + default_renderer += 'colab' +except ImportError: + pass + +# Check Kaggle +if os.path.exists('/kaggle/input'): + default_renderer = 'kaggle' + +# Set default renderer +renderers.default = default_renderer From 8342680db40676b5cf873de33f8e7034ce5d7ab5 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 16 Mar 2019 15:02:41 -0400 Subject: [PATCH 02/48] rename 'default' to 'plotly_mimetype' --- plotly/io/_renderers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index b63d8459161..7be3dfb3a8b 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -529,7 +529,7 @@ def show(fig, renderer=None, validate=True): # Plotly mime type plotly_renderer = PlotlyRenderer() -renderers['default'] = plotly_renderer +renderers['plotly_mimetype'] = plotly_renderer renderers['jupyterlab'] = plotly_renderer renderers['nteract'] = plotly_renderer renderers['vscode'] = plotly_renderer @@ -553,12 +553,12 @@ def show(fig, renderer=None, validate=True): renderers['pdf'] = PdfRenderer(**img_kwargs) # Set default renderer -default_renderer = 'default' +default_renderer = 'plotly_mimetype' # Check Colab try: import google.colab - default_renderer += 'colab' + default_renderer = 'colab' except ImportError: pass From 2cc4f8957f205e94f0aecbd606c2a73f6cc96bf0 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 16 Mar 2019 15:03:22 -0400 Subject: [PATCH 03/48] Added _repr_mimebundle_ --- plotly/basedatatypes.py | 13 +++++++++++++ plotly/io/_renderers.py | 6 +++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 799cd41fc48..3d6709e20cc 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -392,6 +392,19 @@ def __repr__(self): return repr_str + def _repr_mimebundle_(self, include, exclude, **kwargs): + """ + repr_mimebundle should accept include, exclude and **kwargs + """ + import plotly.io as pio + data = pio.renderers._build_mime_bundle(self.to_dict()) + + if include: + data = {k: v for (k, v) in data.items() if k in include} + if exclude: + data = {k: v for (k, v) in data.items() if k not in exclude} + return data + def update(self, dict1=None, **kwargs): """ Update the properties of the figure with a dict and/or with diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index 7be3dfb3a8b..ac2c6cb628e 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -524,9 +524,6 @@ def show(fig, renderer=None, validate=True): # Register renderers -# JSON -renderers['json'] = JsonRenderer() - # Plotly mime type plotly_renderer = PlotlyRenderer() renderers['plotly_mimetype'] = plotly_renderer @@ -542,6 +539,9 @@ def show(fig, renderer=None, validate=True): renderers['kaggle'] = KaggleRenderer(config=config) renderers['colab'] = ColabRenderer(config=config) +# JSON +renderers['json'] = JsonRenderer() + # Static Image img_kwargs = dict(height=450, width=700) renderers['png'] = PngRenderer(**img_kwargs) From 28a669cbc459949f26779f520c91ef32c71bdbca Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 16 Mar 2019 19:00:12 -0400 Subject: [PATCH 04/48] Add initial browser renderer to show figure in browser tab Avoids writing out tmp file by create single use web server to serve the html content to the browser. --- plotly/io/_renderers.py | 107 ++++++++++++++++++++++++++++++++++------ 1 file changed, 93 insertions(+), 14 deletions(-) diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index ac2c6cb628e..17ab27485a9 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -6,6 +6,7 @@ import uuid import six import os +import webbrowser from IPython.display import display_html, display @@ -43,9 +44,11 @@ def __getitem__(self, item): return renderer def __setitem__(self, key, value): - if not isinstance(value, MimetypeRenderer): - raise ValueError( - 'Renderer must be a subclass of MimetypeRenderer') + if not isinstance(value, (MimetypeRenderer, SideEffectRenderer)): + raise ValueError("""\ +Renderer must be a subclass of MimetypeRenderer or SideEffectRenderer. + Received value with type: {typ}""".format(typ=type(value))) + self._renderers[key] = value def __delitem__(self, key): @@ -130,7 +133,6 @@ def _validate_coerce_renderers(self, renderers_string): return renderer_names - def __repr__(self): return """\ Renderers configuration @@ -155,23 +157,36 @@ def _available_templates_str(self): return available def _build_mime_bundle(self, fig_dict, renderers_string=None): - mime_renderers = True - assert mime_renderers - if renderers_string: renderer_names = self._validate_coerce_renderers(renderers_string) renderers_list = [self[name] for name in renderer_names] for renderer in renderers_list: - renderer.activate() + if isinstance(renderer, MimetypeRenderer): + renderer.activate() else: renderers_list = self._default_renderers bundle = {} for renderer in renderers_list: - bundle.update(renderer.to_mimebundle(fig_dict)) + if isinstance(renderer, MimetypeRenderer): + bundle.update(renderer.to_mimebundle(fig_dict)) return bundle + def _perform_side_effect_rendering(self, fig_dict, renderers_string=None): + if renderers_string: + renderer_names = self._validate_coerce_renderers(renderers_string) + renderers_list = [self[name] for name in renderer_names] + for renderer in renderers_list: + if isinstance(renderer, SideEffectRenderer): + renderer.activate() + else: + renderers_list = self._default_renderers + + for renderer in renderers_list: + if isinstance(renderer, SideEffectRenderer): + renderer.render(fig_dict) + # Make config a singleton object # ------------------------------ @@ -180,7 +195,6 @@ def _build_mime_bundle(self, fig_dict, renderers_string=None): class MimetypeRenderer(object): - def activate(self): pass @@ -511,16 +525,78 @@ def __init__(self, config=None, auto_play=False): auto_play=auto_play) +class SideEffectRenderer(object): + + def activate(self): + pass + + def render(self, fig): + raise NotImplementedError() + + +def open_html_in_browser(html): + """Display html in the default web browser without creating a temp file. + + Instantiates a trivial http server and calls webbrowser.open with a URL + to retrieve html from that server. + """ + from http.server import BaseHTTPRequestHandler, HTTPServer + + if isinstance(html, six.string_types): + html = html.encode('utf8') + + class OneShotRequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + + bufferSize = 1024*1024 + for i in range(0, len(html), bufferSize): + self.wfile.write(html[i:i+bufferSize]) + + def log_message(self, format, *args): + # Silence stderr logging + pass + + server = HTTPServer(('127.0.0.1', 0), OneShotRequestHandler) + webbrowser.open('http://127.0.0.1:%s' % server.server_port) + server.handle_request() + + +class BrowserRenderer(SideEffectRenderer): + def __init__(self, config=None, auto_play=False): + self.config = config + self.auto_play = auto_play + + def render(self, fig_dict): + renderer = HtmlRenderer( + connected=False, + fullhtml=True, + requirejs=False, + global_init=False, + config=self.config, + auto_play=self.auto_play) + + bundle = renderer.to_mimebundle(fig_dict) + html = bundle['text/html'] + open_html_in_browser(html) + + # Show def show(fig, renderer=None, validate=True): fig_dict = validate_coerce_fig_to_dict(fig, validate) - mime_renderers = True - if mime_renderers: - bundle = renderers._build_mime_bundle( - fig_dict, renderers_string=renderer) + # Mimetype renderers + bundle = renderers._build_mime_bundle( + fig_dict, renderers_string=renderer) + if bundle: display(bundle, raw=True) + # Side effect renderers + renderers._perform_side_effect_rendering( + fig_dict, renderers_string=renderer) + # Register renderers @@ -552,6 +628,9 @@ def show(fig, renderer=None, validate=True): renderers['svg'] = SvgRenderer(**img_kwargs) renderers['pdf'] = PdfRenderer(**img_kwargs) +# Side effects +renderers['browser'] = BrowserRenderer(config=config) + # Set default renderer default_renderer = 'plotly_mimetype' From 8bf793f4d954c4bf74bbf1d692f63cd96312cbe9 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 17 Mar 2019 06:25:19 -0400 Subject: [PATCH 05/48] Expose renderer base classes --- plotly/io/__init__.py | 2 + plotly/io/_base_renderers.py | 415 +++++++++++++++++++++++++++++++++++ plotly/io/_renderers.py | 407 +--------------------------------- plotly/io/base_renderers.py | 5 + 4 files changed, 431 insertions(+), 398 deletions(-) create mode 100644 plotly/io/_base_renderers.py create mode 100644 plotly/io/base_renderers.py diff --git a/plotly/io/__init__.py b/plotly/io/__init__.py index 6b395047200..063346ec9c9 100644 --- a/plotly/io/__init__.py +++ b/plotly/io/__init__.py @@ -6,3 +6,5 @@ from ._templates import templates, to_templated from ._renderers import renderers, show + +from . import base_renderers diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py new file mode 100644 index 00000000000..a7b6b0ad4a3 --- /dev/null +++ b/plotly/io/_base_renderers.py @@ -0,0 +1,415 @@ +import base64 +import json +import webbrowser +import uuid + +from IPython.display import display_html +import six + +from plotly.io import to_json, to_image +from plotly import utils +from plotly.io._orca import ensure_server +from plotly.offline.offline import _get_jconfig, get_plotlyjs + + +class MimetypeRenderer(object): + def activate(self): + pass + + def to_mimebundle(self, fig_dict): + raise NotImplementedError() + + +class JsonRenderer(MimetypeRenderer): + def to_mimebundle(self, fig_dict): + value = json.loads(to_json(fig_dict)) + return {'application/json': value} + + +# Plotly mimetype +class PlotlyRenderer(MimetypeRenderer): + def __init__(self, config=None): + self.config = dict(config) if config else {} + + def to_mimebundle(self, fig_dict): + config = _get_jconfig(self.config) + if config: + fig_dict['config'] = config + return {'application/vnd.plotly.v1+json': fig_dict} + + +# Static Image +class ImageRenderer(MimetypeRenderer): + def __init__(self, + mime_type, + b64_encode=False, + format=None, + width=None, + height=None, + scale=None): + + self.mime_type = mime_type + self.b64_encode = b64_encode + self.format = format + self.width = width + self.height = height + self.scale = scale + + def activate(self): + ensure_server() + + def to_mimebundle(self, fig_dict): + image_bytes = to_image( + fig_dict, + format=self.format, + width=self.width, + height=self.height, + scale=self.scale) + + if self.b64_encode: + image_str = base64.b64encode(image_bytes).decode('utf8') + else: + image_str = image_bytes.decode('utf8') + + return {self.mime_type: image_str} + + +class PngRenderer(ImageRenderer): + def __init__(self, width=None, height=None, scale=None): + super(PngRenderer, self).__init__( + mime_type='image/png', + b64_encode=True, + format='png', + width=width, + height=height, + scale=scale) + + +class SvgRenderer(ImageRenderer): + def __init__(self, width=None, height=None, scale=None): + super(SvgRenderer, self).__init__( + mime_type='image/svg+xml', + b64_encode=False, + format='svg', + width=width, + height=height, + scale=scale) + + +class PdfRenderer(ImageRenderer): + def __init__(self, width=None, height=None, scale=None): + super(PdfRenderer, self).__init__( + mime_type='application/pdf', + b64_encode=True, + format='pdf', + width=width, + height=height, + scale=scale) + + +class JpegRenderer(ImageRenderer): + def __init__(self, width=None, height=None, scale=None): + super(JpegRenderer, self).__init__( + mime_type='image/jpeg', + b64_encode=True, + format='jpg', + width=width, + height=height, + scale=scale) + + +# HTML +# Build script to set global PlotlyConfig object. This must execute before +# plotly.js is loaded. +_window_plotly_config = """\ +window.PlotlyConfig = {MathJaxConfig: 'local'};""" + + +class HtmlRenderer(MimetypeRenderer): + + def __init__(self, + connected=False, + fullhtml=False, + requirejs=True, + global_init=False, + config=None, + auto_play=False): + + self.config = dict(config) if config else {} + self.auto_play = auto_play + self.connected = connected + self.global_init = global_init + self.requirejs = requirejs + self.fullhtml = fullhtml + + def activate(self): + if self.global_init: + if not self.requirejs: + raise ValueError( + 'global_init is only supported with requirejs=True') + + if self.connected: + # Connected so we configure requirejs with the plotly CDN + script = """\ + + """.format(win_config=_window_plotly_config) + + else: + # If not connected then we embed a copy of the plotly.js + # library in the notebook + script = """\ + + """.format(script=get_plotlyjs(), win_config=_window_plotly_config) + + display_html(script, raw=True) + + def to_mimebundle(self, fig_dict): + plotdivid = uuid.uuid4() + + # Serialize figure + jdata = json.dumps( + fig_dict.get('data', []), cls=utils.PlotlyJSONEncoder) + jlayout = json.dumps( + fig_dict.get('layout', {}), cls=utils.PlotlyJSONEncoder) + + if fig_dict.get('frames', None): + jframes = json.dumps( + fig_dict.get('frames', []), cls=utils.PlotlyJSONEncoder) + else: + jframes = None + + # Serialize figure config + config = _get_jconfig(self.config) + jconfig = json.dumps(config) + + # Platform URL + plotly_platform_url = config.get('plotly_domain', 'https://plot.ly') + + # Build script body + if jframes: + if self.auto_play: + animate = """.then(function(){{ + Plotly.animate('{id}'); + }}""".format(id=plotdivid) + + else: + animate = '' + + script = ''' + if (document.getElementById("{id}")) {{ + Plotly.plot( + '{id}', + {data}, + {layout}, + {config} + ).then(function () {add_frames}){animate} + }} + '''.format( + id=plotdivid, + data=jdata, + layout=jlayout, + config=jconfig, + add_frames="{" + "return Plotly.addFrames('{id}',{frames}".format( + id=plotdivid, frames=jframes + ) + ");}", + animate=animate + ) + else: + script = """ + if (document.getElementById("{id}")) {{ + Plotly.newPlot("{id}", {data}, {layout}, {config}); + }} + """.format( + id=plotdivid, + data=jdata, + layout=jlayout, + config=jconfig) + + # Build div + + # Handle require + if self.requirejs: + require_start = 'require(["plotly"], function(Plotly) {' + require_end = '});' + load_plotlyjs = '' + else: + require_start = '' + require_end = '' + if self.connected: + load_plotlyjs = """\ + +""".format( + win_config=_window_plotly_config) + else: + load_plotlyjs = """\ + +""".format(win_config=_window_plotly_config, plotlyjs=get_plotlyjs()) + + # Handle fullhtml + if self.fullhtml: + html_start = '' + html_end = '' + else: + html_start = '' + html_end = '' + + plotly_html_div = """ +{html_start} + {load_plotlyjs} +
+ +{html_end}""".format( + html_start=html_start, + load_plotlyjs=load_plotlyjs, + id=plotdivid, + plotly_platform_url=plotly_platform_url, + require_start=require_start, + script=script, + require_end=require_end, + html_end=html_end) + + return {'text/html': plotly_html_div} + + +class NotebookRenderer(HtmlRenderer): + def __init__(self, connected=False, config=None, auto_play=False): + super(NotebookRenderer, self).__init__( + connected=connected, + fullhtml=False, + requirejs=True, + global_init=True, + config=config, + auto_play=auto_play) + + +class KaggleRenderer(HtmlRenderer): + """ + Same as NotebookRenderer but with connected True + """ + def __init__(self, config=None, auto_play=False): + super(KaggleRenderer, self).__init__( + connected=True, + fullhtml=False, + requirejs=True, + global_init=True, + config=config, + auto_play=auto_play) + + +class ColabRenderer(HtmlRenderer): + """ + Google Colab compatible HTML renderer + """ + def __init__(self, config=None, auto_play=False): + super(ColabRenderer, self).__init__( + connected=True, + fullhtml=True, + requirejs=False, + global_init=False, + config=config, + auto_play=auto_play) + + +class SideEffectRenderer(object): + + def activate(self): + pass + + def render(self, fig): + raise NotImplementedError() + + +def open_html_in_browser(html, using=None, new=0, autoraise=True): + """Display html in the default web browser without creating a temp file. + + Instantiates a trivial http server and calls webbrowser.open with a URL + to retrieve html from that server. + """ + from http.server import BaseHTTPRequestHandler, HTTPServer + + if isinstance(html, six.string_types): + html = html.encode('utf8') + + class OneShotRequestHandler(BaseHTTPRequestHandler): + def do_GET(self): + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + + bufferSize = 1024*1024 + for i in range(0, len(html), bufferSize): + self.wfile.write(html[i:i+bufferSize]) + + def log_message(self, format, *args): + # Silence stderr logging + pass + + server = HTTPServer(('127.0.0.1', 0), OneShotRequestHandler) + webbrowser.get(using).open( + 'http://127.0.0.1:%s' % server.server_port, + new=new, + autoraise=autoraise) + + server.handle_request() + + +class BrowserRenderer(SideEffectRenderer): + def __init__(self, + config=None, + auto_play=False, + using=None, + new=0, + autoraise=True): + + # TODO: add browser name and open num here + # define + self.config = config + self.auto_play = auto_play + self.using = using + self.new = new + self.autoraise = autoraise + + def render(self, fig_dict): + renderer = HtmlRenderer( + connected=False, + fullhtml=True, + requirejs=False, + global_init=False, + config=self.config, + auto_play=self.auto_play) + + bundle = renderer.to_mimebundle(fig_dict) + html = bundle['text/html'] + open_html_in_browser(html, self.using, self.new, self.autoraise) diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index 17ab27485a9..1d987a5a932 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -1,19 +1,15 @@ from __future__ import absolute_import, division -import base64 -import json import textwrap -import uuid import six import os -import webbrowser -from IPython.display import display_html, display +from IPython.display import display -from plotly import utils -from plotly.io import to_json, to_image -from plotly.io._orca import ensure_server -from plotly.offline.offline import _get_jconfig, get_plotlyjs +from plotly.io._base_renderers import ( + MimetypeRenderer, SideEffectRenderer, PlotlyRenderer, NotebookRenderer, + KaggleRenderer, ColabRenderer, JsonRenderer, PngRenderer, JpegRenderer, + SvgRenderer, PdfRenderer, BrowserRenderer) from plotly.io._utils import validate_coerce_fig_to_dict @@ -23,6 +19,7 @@ class RenderersConfig(object): """ Singleton object containing the current renderer configurations """ + def __init__(self): self._renderers = {} self._default_name = None @@ -194,395 +191,6 @@ def _perform_side_effect_rendering(self, fig_dict, renderers_string=None): del RenderersConfig -class MimetypeRenderer(object): - def activate(self): - pass - - def to_mimebundle(self, fig_dict): - raise NotImplementedError() - - -# JSON -class JsonRenderer(MimetypeRenderer): - def to_mimebundle(self, fig_dict): - value = json.loads(to_json(fig_dict)) - return {'application/json': value} - - -# Plotly mimetype -class PlotlyRenderer(MimetypeRenderer): - def __init__(self, config=None): - self.config = dict(config) if config else {} - - def to_mimebundle(self, fig_dict): - config = _get_jconfig(self.config) - if config: - fig_dict['config'] = config - return {'application/vnd.plotly.v1+json': fig_dict} - - -# Static Image -class ImageRenderer(MimetypeRenderer): - def __init__(self, - mime_type, - b64_encode=False, - format=None, - width=None, - height=None, - scale=None): - - self.mime_type = mime_type - self.b64_encode = b64_encode - self.format = format - self.width = width - self.height = height - self.scale = scale - - def activate(self): - ensure_server() - - def to_mimebundle(self, fig_dict): - image_bytes = to_image( - fig_dict, - format=self.format, - width=self.width, - height=self.height, - scale=self.scale) - - if self.b64_encode: - image_str = base64.b64encode(image_bytes).decode('utf8') - else: - image_str = image_bytes.decode('utf8') - - return {self.mime_type: image_str} - - -class PngRenderer(ImageRenderer): - def __init__(self, width=None, height=None, scale=None): - super(PngRenderer, self).__init__( - mime_type='image/png', - b64_encode=True, - format='png', - width=width, - height=height, - scale=scale) - - -class SvgRenderer(ImageRenderer): - def __init__(self, width=None, height=None, scale=None): - super(SvgRenderer, self).__init__( - mime_type='image/svg+xml', - b64_encode=False, - format='svg', - width=width, - height=height, - scale=scale) - - -class PdfRenderer(ImageRenderer): - def __init__(self, width=None, height=None, scale=None): - super(PdfRenderer, self).__init__( - mime_type='application/pdf', - b64_encode=True, - format='pdf', - width=width, - height=height, - scale=scale) - - -class JpegRenderer(ImageRenderer): - def __init__(self, width=None, height=None, scale=None): - super(JpegRenderer, self).__init__( - mime_type='image/jpeg', - b64_encode=True, - format='jpg', - width=width, - height=height, - scale=scale) - - -# HTML -# Build script to set global PlotlyConfig object. This must execute before -# plotly.js is loaded. -_window_plotly_config = """\ -window.PlotlyConfig = {MathJaxConfig: 'local'};""" - - -class HtmlRenderer(MimetypeRenderer): - - def __init__(self, - connected=False, - fullhtml=False, - requirejs=True, - global_init=False, - config=None, - auto_play=False): - - self.config = dict(config) if config else {} - self.auto_play = auto_play - self.connected = connected - self.global_init = global_init - self.requirejs = requirejs - self.fullhtml = fullhtml - - def activate(self): - if self.global_init: - if not self.requirejs: - raise ValueError( - 'global_init is only supported with requirejs=True') - - if self.connected: - # Connected so we configure requirejs with the plotly CDN - script = """\ - - """.format(win_config=_window_plotly_config) - - else: - # If not connected then we embed a copy of the plotly.js - # library in the notebook - script = """\ - - """.format(script=get_plotlyjs(), win_config=_window_plotly_config) - - display_html(script, raw=True) - - def to_mimebundle(self, fig_dict): - plotdivid = uuid.uuid4() - - # Serialize figure - jdata = json.dumps( - fig_dict.get('data', []), cls=utils.PlotlyJSONEncoder) - jlayout = json.dumps( - fig_dict.get('layout', {}), cls=utils.PlotlyJSONEncoder) - - if fig_dict.get('frames', None): - jframes = json.dumps( - fig_dict.get('frames', []), cls=utils.PlotlyJSONEncoder) - else: - jframes = None - - # Serialize figure config - config = _get_jconfig(self.config) - jconfig = json.dumps(config) - - # Platform URL - plotly_platform_url = config.get('plotly_domain', 'https://plot.ly') - - # Build script body - if jframes: - if self.auto_play: - animate = """.then(function(){{ - Plotly.animate('{id}'); - }}""".format(id=plotdivid) - - else: - animate = '' - - script = ''' - if (document.getElementById("{id}")) {{ - Plotly.plot( - '{id}', - {data}, - {layout}, - {config} - ).then(function () {add_frames}){animate} - }} - '''.format( - id=plotdivid, - data=jdata, - layout=jlayout, - config=jconfig, - add_frames="{" + "return Plotly.addFrames('{id}',{frames}".format( - id=plotdivid, frames=jframes - ) + ");}", - animate=animate - ) - else: - script = """ - if (document.getElementById("{id}")) {{ - Plotly.newPlot("{id}", {data}, {layout}, {config}); - }} - """.format( - id=plotdivid, - data=jdata, - layout=jlayout, - config=jconfig) - - # Build div - - # Handle require - if self.requirejs: - require_start = 'require(["plotly"], function(Plotly) {' - require_end = '});' - load_plotlyjs = '' - else: - require_start = '' - require_end = '' - if self.connected: - load_plotlyjs = """\ - -""".format( - win_config=_window_plotly_config) - else: - load_plotlyjs = """\ - -""".format(win_config=_window_plotly_config, plotlyjs=get_plotlyjs()) - - # Handle fullhtml - if self.fullhtml: - html_start = '' - html_end = '' - else: - html_start = '' - html_end = '' - - plotly_html_div = """ -{html_start} - {load_plotlyjs} -
- -{html_end}""".format( - html_start=html_start, - load_plotlyjs=load_plotlyjs, - id=plotdivid, - plotly_platform_url=plotly_platform_url, - require_start=require_start, - script=script, - require_end=require_end, - html_end=html_end) - - return {'text/html': plotly_html_div} - - -class NotebookRenderer(HtmlRenderer): - def __init__(self, connected=False, config=None, auto_play=False): - super(NotebookRenderer, self).__init__( - connected=connected, - fullhtml=False, - requirejs=True, - global_init=True, - config=config, - auto_play=auto_play) - - -class KaggleRenderer(HtmlRenderer): - """ - Same as NotebookRenderer but with connected True - """ - def __init__(self, config=None, auto_play=False): - super(KaggleRenderer, self).__init__( - connected=True, - fullhtml=False, - requirejs=True, - global_init=True, - config=config, - auto_play=auto_play) - - -class ColabRenderer(HtmlRenderer): - """ - Google Colab compatible HTML renderer - """ - def __init__(self, config=None, auto_play=False): - super(ColabRenderer, self).__init__( - connected=True, - fullhtml=True, - requirejs=False, - global_init=False, - config=config, - auto_play=auto_play) - - -class SideEffectRenderer(object): - - def activate(self): - pass - - def render(self, fig): - raise NotImplementedError() - - -def open_html_in_browser(html): - """Display html in the default web browser without creating a temp file. - - Instantiates a trivial http server and calls webbrowser.open with a URL - to retrieve html from that server. - """ - from http.server import BaseHTTPRequestHandler, HTTPServer - - if isinstance(html, six.string_types): - html = html.encode('utf8') - - class OneShotRequestHandler(BaseHTTPRequestHandler): - def do_GET(self): - self.send_response(200) - self.send_header("Content-type", "text/html") - self.end_headers() - - bufferSize = 1024*1024 - for i in range(0, len(html), bufferSize): - self.wfile.write(html[i:i+bufferSize]) - - def log_message(self, format, *args): - # Silence stderr logging - pass - - server = HTTPServer(('127.0.0.1', 0), OneShotRequestHandler) - webbrowser.open('http://127.0.0.1:%s' % server.server_port) - server.handle_request() - - -class BrowserRenderer(SideEffectRenderer): - def __init__(self, config=None, auto_play=False): - self.config = config - self.auto_play = auto_play - - def render(self, fig_dict): - renderer = HtmlRenderer( - connected=False, - fullhtml=True, - requirejs=False, - global_init=False, - config=self.config, - auto_play=self.auto_play) - - bundle = renderer.to_mimebundle(fig_dict) - html = bundle['text/html'] - open_html_in_browser(html) - - # Show def show(fig, renderer=None, validate=True): fig_dict = validate_coerce_fig_to_dict(fig, validate) @@ -630,6 +238,8 @@ def show(fig, renderer=None, validate=True): # Side effects renderers['browser'] = BrowserRenderer(config=config) +renderers['firefox'] = BrowserRenderer(config=config, using='firefox') +renderers['chrome'] = BrowserRenderer(config=config, using='chrome') # Set default renderer default_renderer = 'plotly_mimetype' @@ -637,6 +247,7 @@ def show(fig, renderer=None, validate=True): # Check Colab try: import google.colab + default_renderer = 'colab' except ImportError: pass diff --git a/plotly/io/base_renderers.py b/plotly/io/base_renderers.py new file mode 100644 index 00000000000..e3ceb0c8906 --- /dev/null +++ b/plotly/io/base_renderers.py @@ -0,0 +1,5 @@ +from ._base_renderers import ( + MimetypeRenderer, PlotlyRenderer, JsonRenderer, ImageRenderer, + PngRenderer, SvgRenderer, PdfRenderer, JpegRenderer, HtmlRenderer, + ColabRenderer, KaggleRenderer, NotebookRenderer, SideEffectRenderer, + BrowserRenderer) From 3dfb24a712a1ab58697a1f806ad3f499d60bff96 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sun, 17 Mar 2019 06:53:40 -0400 Subject: [PATCH 06/48] Documentation / cleanup of _renderers --- plotly/io/_renderers.py | 96 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 89 insertions(+), 7 deletions(-) diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index 1d987a5a932..9df8facf886 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -19,7 +19,6 @@ class RenderersConfig(object): """ Singleton object containing the current renderer configurations """ - def __init__(self): self._renderers = {} self._default_name = None @@ -89,6 +88,13 @@ def default(self): figures when they are displayed in a jupyter notebook or when using the plotly.io.show function + Multiple renderers may be registered by separating their names with + '+' characters. For example, to specify rendering compatible with + the classic Jupyter Notebook, JupyterLab, and PDF export: + + >>> import plotly.io as pio + >>> pio.renderers.default = 'notebook+jupyterlab+pdf' + The names of available templates may be retrieved with: >>> import plotly.io as pio @@ -118,6 +124,19 @@ def default(self, value): renderer.activate() def _validate_coerce_renderers(self, renderers_string): + """ + Input a string and validate that it contains the names of one or more + valid renderers separated on '+' characters. If valid, return + a list of the renderer names + + Parameters + ---------- + renderers_string: str + + Returns + ------- + list of str + """ # Validate value if not isinstance(renderers_string, six.string_types): raise ValueError('Renderer must be specified as a string') @@ -143,7 +162,7 @@ def __repr__(self): def _available_templates_str(self): """ Return nicely wrapped string representation of all - available template names + available renderer names """ available = '\n'.join(textwrap.wrap( repr(list(self)), @@ -154,6 +173,26 @@ def _available_templates_str(self): return available def _build_mime_bundle(self, fig_dict, renderers_string=None): + """ + Build a mime bundle dict containing a kev/value pair for each + MimetypeRenderer specified in either the default renderer string, + or in the supplied renderers_string argument. + + Note that this method skips any renderers that are not subclasses + of MimetypeRenderer. + + Parameters + ---------- + fig_dict: dict + Figure dictionary + renderers_string: str or None (default None) + Renderer string to process rather than the current default + renderer string + + Returns + ------- + dict + """ if renderers_string: renderer_names = self._validate_coerce_renderers(renderers_string) renderers_list = [self[name] for name in renderer_names] @@ -171,6 +210,26 @@ def _build_mime_bundle(self, fig_dict, renderers_string=None): return bundle def _perform_side_effect_rendering(self, fig_dict, renderers_string=None): + """ + Perform side-effect rendering for each SideEffectRenderer specified + in either the default renderer string, or in the supplied + renderers_string argument. + + Note that this method skips any renderers that are not subclasses + of SideEffectRenderer. + + Parameters + ---------- + fig_dict: dict + Figure dictionary + renderers_string: str or None (default None) + Renderer string to process rather than the current default + renderer string + + Returns + ------- + None + """ if renderers_string: renderer_names = self._validate_coerce_renderers(renderers_string) renderers_list = [self[name] for name in renderer_names] @@ -185,14 +244,36 @@ def _perform_side_effect_rendering(self, fig_dict, renderers_string=None): renderer.render(fig_dict) -# Make config a singleton object -# ------------------------------ +# Make renderers a singleton object +# --------------------------------- renderers = RenderersConfig() del RenderersConfig # Show def show(fig, renderer=None, validate=True): + """ + Show a figure using either the default renderer(s) or the renderer(s) + specified by the renderer argument + + Parameters + ---------- + fig: dict of Figure + The Figure object or figure dict to display + + renderer: str or None (default None) + A string containing the names of one or more registered renderers + (separated by '+' characters) or None. If None, then the default + renderers specified in plotly.io.renderers.default are used. + + validate: bool (default True) + True if the figure should be validated before being shown, + False otherwise. + + Returns + ------- + None + """ fig_dict = validate_coerce_fig_to_dict(fig, validate) # Mimetype renderers @@ -207,6 +288,7 @@ def show(fig, renderer=None, validate=True): # Register renderers +# ------------------ # Plotly mime type plotly_renderer = PlotlyRenderer() @@ -232,7 +314,6 @@ def show(fig, renderer=None, validate=True): jpeg_renderer = JpegRenderer(**img_kwargs) renderers['jpeg'] = jpeg_renderer renderers['jpg'] = jpeg_renderer - renderers['svg'] = SvgRenderer(**img_kwargs) renderers['pdf'] = PdfRenderer(**img_kwargs) @@ -242,9 +323,10 @@ def show(fig, renderer=None, validate=True): renderers['chrome'] = BrowserRenderer(config=config, using='chrome') # Set default renderer +# -------------------- default_renderer = 'plotly_mimetype' -# Check Colab +# Check if we're running in Google Colab try: import google.colab @@ -252,7 +334,7 @@ def show(fig, renderer=None, validate=True): except ImportError: pass -# Check Kaggle +# Check if we're running in a Kaggle notebook if os.path.exists('/kaggle/input'): default_renderer = 'kaggle' From 2dd92c072d96611b1c382da79c383d8d048141eb Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 18 Mar 2019 08:32:52 -0400 Subject: [PATCH 07/48] Auto-detect VSCode environment and default to renderer combination of 'notebook_connected+plotly_mimetype' This renderer combination will work automatically in the classic notebook, jupyterlab, nteract, vscode, and nbconvert HTML export. We use 'notebook_connected' rather than 'notebook' to avoid bloating notebook size. This comes at the cost of requiring internet connectivity, but that is a preferable trade-off to adding ~3MB to each saved notebook. --- plotly/io/_renderers.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index 9df8facf886..30636a9b076 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -324,19 +324,30 @@ def show(fig, renderer=None, validate=True): # Set default renderer # -------------------- -default_renderer = 'plotly_mimetype' +default_renderer = None -# Check if we're running in Google Colab +# Try to detect environment so that we can enable a useful default renderer try: import google.colab - default_renderer = 'colab' except ImportError: pass # Check if we're running in a Kaggle notebook -if os.path.exists('/kaggle/input'): +if not default_renderer and os.path.exists('/kaggle/input'): default_renderer = 'kaggle' +# Check if we're running in VSCode +if not default_renderer and 'VSCODE_PID' in os.environ: + default_renderer = 'vscode' + +# Fallback to renderer combination that will work automatically in the +# classic notebook, jupyterlab, nteract, vscode, and nbconvert HTML export. +# We use 'notebook_connected' rather than 'notebook' to avoid bloating +# notebook size. This comes at the cost of requiring internet connectivity, +# but that is a preferable trade-off to adding ~3MB to each saved notebook. +if not default_renderer: + default_renderer = 'notebook_connected+plotly_mimetype' + # Set default renderer renderers.default = default_renderer From 953bae8c480c9cc02c7fab6aea3940d5139c8943 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 18 Mar 2019 08:37:29 -0400 Subject: [PATCH 08/48] Documentation / cleanup of _base_renderers --- plotly/io/_base_renderers.py | 151 +++++++++++++++++++++++++++++++---- 1 file changed, 135 insertions(+), 16 deletions(-) diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index a7b6b0ad4a3..ba75bd2974b 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -2,6 +2,7 @@ import json import webbrowser import uuid +import inspect from IPython.display import display_html import six @@ -12,7 +13,23 @@ from plotly.offline.offline import _get_jconfig, get_plotlyjs -class MimetypeRenderer(object): +class RendererRepr(object): + """ + A mixin implementing a simple __repr__ for Renderer classes + """ + def __repr__(self): + init_sig = inspect.signature(self.__init__) + init_args = list(init_sig.parameters.keys()) + return "{cls}({attrs})".format( + cls=self.__class__.__name__, + attrs=", ".join("{}={!r}".format(k, self.__dict__[k]) + for k in init_args)) + + +class MimetypeRenderer(RendererRepr): + """ + Base class for all mime type renderers + """ def activate(self): pass @@ -21,6 +38,12 @@ def to_mimebundle(self, fig_dict): class JsonRenderer(MimetypeRenderer): + """ + Renderer to display figures as JSON hierarchies. This renderer is + compatible with JupyterLab and VSCode. + + mime type: 'application/json' + """ def to_mimebundle(self, fig_dict): value = json.loads(to_json(fig_dict)) return {'application/json': value} @@ -28,6 +51,13 @@ def to_mimebundle(self, fig_dict): # Plotly mimetype class PlotlyRenderer(MimetypeRenderer): + """ + Renderer to display figures using the plotly mime type. This renderer is + compatible with JupyterLab (using the @jupyterlab/plotly-extension), + VSCode, and nteract. + + mime type: 'application/vnd.plotly.v1+json' + """ def __init__(self, config=None): self.config = dict(config) if config else {} @@ -40,6 +70,9 @@ def to_mimebundle(self, fig_dict): # Static Image class ImageRenderer(MimetypeRenderer): + """ + Base class for all static image renderers + """ def __init__(self, mime_type, b64_encode=False, @@ -56,6 +89,7 @@ def __init__(self, self.scale = scale def activate(self): + # Start up orca server to reduce the delay on first render ensure_server() def to_mimebundle(self, fig_dict): @@ -75,6 +109,14 @@ def to_mimebundle(self, fig_dict): class PngRenderer(ImageRenderer): + """ + Renderer to display figures as static PNG images. This renderer requires + the orca command-line utility and is broadly compatible across IPython + environments (classic Jupyter Notebook, JupyterLab, QtConsole, VSCode, + PyCharm, etc) and nbconvert targets (HTML, PDF, etc.). + + mime type: 'image/png' + """ def __init__(self, width=None, height=None, scale=None): super(PngRenderer, self).__init__( mime_type='image/png', @@ -86,6 +128,14 @@ def __init__(self, width=None, height=None, scale=None): class SvgRenderer(ImageRenderer): + """ + Renderer to display figures as static SVG images. This renderer requires + the orca command-line utility and is broadly compatible across IPython + environments (classic Jupyter Notebook, JupyterLab, QtConsole, VSCode, + PyCharm, etc) and nbconvert targets (HTML, PDF, etc.). + + mime type: 'image/svg+xml' + """ def __init__(self, width=None, height=None, scale=None): super(SvgRenderer, self).__init__( mime_type='image/svg+xml', @@ -96,23 +146,38 @@ def __init__(self, width=None, height=None, scale=None): scale=scale) -class PdfRenderer(ImageRenderer): +class JpegRenderer(ImageRenderer): + """ + Renderer to display figures as static JPEG images. This renderer requires + the orca command-line utility and is broadly compatible across IPython + environments (classic Jupyter Notebook, JupyterLab, QtConsole, VSCode, + PyCharm, etc) and nbconvert targets (HTML, PDF, etc.). + + mime type: 'image/jpeg' + """ def __init__(self, width=None, height=None, scale=None): - super(PdfRenderer, self).__init__( - mime_type='application/pdf', + super(JpegRenderer, self).__init__( + mime_type='image/jpeg', b64_encode=True, - format='pdf', + format='jpg', width=width, height=height, scale=scale) -class JpegRenderer(ImageRenderer): +class PdfRenderer(ImageRenderer): + """ + Renderer to display figures as static PDF images. This renderer requires + the orca command-line utility and is compatible with JupyterLab and the + LaTeX-based nbconvert export to PDF. + + mime type: 'application/pdf' + """ def __init__(self, width=None, height=None, scale=None): - super(JpegRenderer, self).__init__( - mime_type='image/jpeg', + super(PdfRenderer, self).__init__( + mime_type='application/pdf', b64_encode=True, - format='jpg', + format='pdf', width=width, height=height, scale=scale) @@ -126,7 +191,11 @@ def __init__(self, width=None, height=None, scale=None): class HtmlRenderer(MimetypeRenderer): + """ + Base class for all HTML mime type renderers + mime type: 'text/html' + """ def __init__(self, connected=False, fullhtml=False, @@ -304,6 +373,17 @@ def to_mimebundle(self, fig_dict): class NotebookRenderer(HtmlRenderer): + """ + Renderer to display interactive figures in the classic Jupyter Notebook. + This renderer is also useful for notebooks that will be converted to + HTML using nbconvert/nbviewer as it will produce standalone HTML files + that include interactive figures. + + This renderer automatically performs global notebook initialization when + activated. + + mime type: 'text/html' + """ def __init__(self, connected=False, config=None, auto_play=False): super(NotebookRenderer, self).__init__( connected=connected, @@ -316,7 +396,14 @@ def __init__(self, connected=False, config=None, auto_play=False): class KaggleRenderer(HtmlRenderer): """ - Same as NotebookRenderer but with connected True + Renderer to display interactive figures in Kaggle Notebooks. + + Same as NotebookRenderer but with connected=True so that the plotly.js + bundle is loaded from a CDN rather than being embedded in the notebook. + + This renderer is enabled by default when running in a Kaggle notebook. + + mime type: 'text/html' """ def __init__(self, config=None, auto_play=False): super(KaggleRenderer, self).__init__( @@ -330,7 +417,11 @@ def __init__(self, config=None, auto_play=False): class ColabRenderer(HtmlRenderer): """ - Google Colab compatible HTML renderer + Renderer to display interactive figures in Google Colab Notebooks. + + This renderer is enabled by default when running in a Colab notebook. + + mime type: 'text/html' """ def __init__(self, config=None, auto_play=False): super(ColabRenderer, self).__init__( @@ -342,8 +433,17 @@ def __init__(self, config=None, auto_play=False): auto_play=auto_play) -class SideEffectRenderer(object): - +class SideEffectRenderer(RendererRepr): + """ + Base class for side-effect renderers. SideEffectRenderer subclasses + do not display figures inline in a notebook environment, but render + figures by some external means (e.g. a separate browser tab). + + Unlike MimetypeRenderer subclasses, SideEffectRenderer subclasses are not + invoked when a figure is asked to display itself in the notebook. + Instead, they are invoked when the plotly.io.show function is called + on a figure. + """ def activate(self): pass @@ -352,10 +452,18 @@ def render(self, fig): def open_html_in_browser(html, using=None, new=0, autoraise=True): - """Display html in the default web browser without creating a temp file. + """ + Display html in a web browser without creating a temp file. - Instantiates a trivial http server and calls webbrowser.open with a URL - to retrieve html from that server. + Instantiates a trivial http server and uses the webbrowser modeul to + open a URL to retrieve html from that server. + + Parameters + ---------- + html: str + HTML string to display + using, new, autoraise: + See docstrings in webbrowser.get and webbrowser.open """ from http.server import BaseHTTPRequestHandler, HTTPServer @@ -386,6 +494,17 @@ def log_message(self, format, *args): class BrowserRenderer(SideEffectRenderer): + """ + Renderer to display interactive figures in an external web browser. + This renderer will open a new browser window or tab when the + plotly.io.show function is called on a figure. + + This renderer has no ipython/jupyter dependencies and is a good choice + for use in environments that do not support the inline display of + interactive figures. + + mime type: 'text/html' + """ def __init__(self, config=None, auto_play=False, From da39c34bc1ae837727b4754dbc302509a8060233 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 18 Mar 2019 10:18:59 -0400 Subject: [PATCH 09/48] Update default renderers explanation --- plotly/io/_renderers.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index 30636a9b076..ba206399d74 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -344,8 +344,11 @@ def show(fig, renderer=None, validate=True): # Fallback to renderer combination that will work automatically in the # classic notebook, jupyterlab, nteract, vscode, and nbconvert HTML export. # We use 'notebook_connected' rather than 'notebook' to avoid bloating -# notebook size. This comes at the cost of requiring internet connectivity, +# notebook size and slowing down plotly.py initial import. +# This comes at the cost of requiring internet connectivity to view, # but that is a preferable trade-off to adding ~3MB to each saved notebook. +# +# Note that this doesn't cause any problem for offline JupyterLab users. if not default_renderer: default_renderer = 'notebook_connected+plotly_mimetype' From c2a6471b716ee8885d24fc153bfbc7c18b1a8033 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 18 Mar 2019 10:19:40 -0400 Subject: [PATCH 10/48] Add renderer docstring to repr for easy discoverability --- plotly/io/_base_renderers.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index ba75bd2974b..2451481364a 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -20,10 +20,11 @@ class RendererRepr(object): def __repr__(self): init_sig = inspect.signature(self.__init__) init_args = list(init_sig.parameters.keys()) - return "{cls}({attrs})".format( + return "{cls}({attrs})\n{doc}".format( cls=self.__class__.__name__, attrs=", ".join("{}={!r}".format(k, self.__dict__[k]) - for k in init_args)) + for k in init_args), + doc=self.__doc__) class MimetypeRenderer(RendererRepr): From e6b9e45e2d06a1c19ab09b2e324488dbdd187045 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 18 Mar 2019 10:56:12 -0400 Subject: [PATCH 11/48] Make ipython an optional dependency --- plotly/io/_base_renderers.py | 13 +++-- plotly/io/_renderers.py | 60 ++++++++++++--------- plotly/tests/test_io/test_mime_renderers.py | 0 3 files changed, 43 insertions(+), 30 deletions(-) create mode 100644 plotly/tests/test_io/test_mime_renderers.py diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index 2451481364a..a474062edc3 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -4,14 +4,14 @@ import uuid import inspect -from IPython.display import display_html import six - from plotly.io import to_json, to_image -from plotly import utils +from plotly import utils, optional_imports from plotly.io._orca import ensure_server from plotly.offline.offline import _get_jconfig, get_plotlyjs +ipython_display = optional_imports.get_module('IPython.display') + class RendererRepr(object): """ @@ -214,6 +214,11 @@ def __init__(self, def activate(self): if self.global_init: + if not ipython_display: + raise ValueError( + 'The {cls} class requires ipython but it is not installed' + .format(cls=self.__class__.__name__)) + if not self.requirejs: raise ValueError( 'global_init is only supported with requirejs=True') @@ -255,7 +260,7 @@ def activate(self): """.format(script=get_plotlyjs(), win_config=_window_plotly_config) - display_html(script, raw=True) + ipython_display.display_html(script, raw=True) def to_mimebundle(self, fig_dict): plotdivid = uuid.uuid4() diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index ba206399d74..04511abca12 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -4,7 +4,7 @@ import six import os -from IPython.display import display +from plotly import optional_imports from plotly.io._base_renderers import ( MimetypeRenderer, SideEffectRenderer, PlotlyRenderer, NotebookRenderer, @@ -12,6 +12,8 @@ SvgRenderer, PdfRenderer, BrowserRenderer) from plotly.io._utils import validate_coerce_fig_to_dict +ipython_display = optional_imports.get_module('IPython.display') + # Renderer configuration class # ----------------------------- @@ -280,7 +282,11 @@ def show(fig, renderer=None, validate=True): bundle = renderers._build_mime_bundle( fig_dict, renderers_string=renderer) if bundle: - display(bundle, raw=True) + if not ipython_display: + raise ValueError( + 'Mime type rendering requires ipython but it is not installed') + + ipython_display.display(bundle, raw=True) # Side effect renderers renderers._perform_side_effect_rendering( @@ -327,30 +333,32 @@ def show(fig, renderer=None, validate=True): default_renderer = None # Try to detect environment so that we can enable a useful default renderer -try: - import google.colab - default_renderer = 'colab' -except ImportError: - pass - -# Check if we're running in a Kaggle notebook -if not default_renderer and os.path.exists('/kaggle/input'): - default_renderer = 'kaggle' - -# Check if we're running in VSCode -if not default_renderer and 'VSCODE_PID' in os.environ: - default_renderer = 'vscode' - -# Fallback to renderer combination that will work automatically in the -# classic notebook, jupyterlab, nteract, vscode, and nbconvert HTML export. -# We use 'notebook_connected' rather than 'notebook' to avoid bloating -# notebook size and slowing down plotly.py initial import. -# This comes at the cost of requiring internet connectivity to view, -# but that is a preferable trade-off to adding ~3MB to each saved notebook. -# -# Note that this doesn't cause any problem for offline JupyterLab users. -if not default_renderer: - default_renderer = 'notebook_connected+plotly_mimetype' +if ipython_display: + try: + import google.colab + default_renderer = 'colab' + except ImportError: + pass + + # Check if we're running in a Kaggle notebook + if not default_renderer and os.path.exists('/kaggle/input'): + default_renderer = 'kaggle' + + # Check if we're running in VSCode + if not default_renderer and 'VSCODE_PID' in os.environ: + default_renderer = 'vscode' + + # Fallback to renderer combination that will work automatically in the + # classic notebook, jupyterlab, nteract, vscode, and nbconvert HTML + # export. We use 'notebook_connected' rather than 'notebook' to avoid + # bloating notebook size and slowing down plotly.py initial import. + # This comes at the cost of requiring internet connectivity to view, + # but that is a preferable trade-off to adding ~3MB to each saved + # notebook. + # + # Note that this doesn't cause any problem for offline JupyterLab users. + if not default_renderer: + default_renderer = 'notebook_connected+plotly_mimetype' # Set default renderer renderers.default = default_renderer diff --git a/plotly/tests/test_io/test_mime_renderers.py b/plotly/tests/test_io/test_mime_renderers.py new file mode 100644 index 00000000000..e69de29bb2d From 54ac011844be6fe0acb375f459d2a8bd0752bf03 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 18 Mar 2019 11:08:25 -0400 Subject: [PATCH 12/48] Python 2.7 support --- plotly/io/_base_renderers.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index a474062edc3..fc30b4b2367 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -12,14 +12,26 @@ ipython_display = optional_imports.get_module('IPython.display') +try: + from http.server import BaseHTTPRequestHandler, HTTPServer +except ImportError: + # Python 2.7 + from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer + class RendererRepr(object): """ A mixin implementing a simple __repr__ for Renderer classes """ def __repr__(self): - init_sig = inspect.signature(self.__init__) - init_args = list(init_sig.parameters.keys()) + try: + init_sig = inspect.signature(self.__init__) + init_args = list(init_sig.parameters.keys()) + except AttributeError: + # Python 2.7 + argspec = inspect.getargspec(self.__init__) + init_args = [a for a in argspec.args if a != 'self'] + return "{cls}({attrs})\n{doc}".format( cls=self.__class__.__name__, attrs=", ".join("{}={!r}".format(k, self.__dict__[k]) @@ -471,8 +483,6 @@ def open_html_in_browser(html, using=None, new=0, autoraise=True): using, new, autoraise: See docstrings in webbrowser.get and webbrowser.open """ - from http.server import BaseHTTPRequestHandler, HTTPServer - if isinstance(html, six.string_types): html = html.encode('utf8') From 7dc10291bc9065bcffcf9fa1c38123e5fa6ed91d Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 18 Mar 2019 15:52:32 -0400 Subject: [PATCH 13/48] Add parent From c23f76e8d89b8f2cebd3be5040c619640393a4e0 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 18 Mar 2019 15:53:31 -0400 Subject: [PATCH 14/48] Added initial renderers test suite --- plotly/tests/test_io/test_mime_renderers.py | 0 plotly/tests/test_io/test_renderers.py | 369 ++++++++++++++++++++ 2 files changed, 369 insertions(+) delete mode 100644 plotly/tests/test_io/test_mime_renderers.py create mode 100644 plotly/tests/test_io/test_renderers.py diff --git a/plotly/tests/test_io/test_mime_renderers.py b/plotly/tests/test_io/test_mime_renderers.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/plotly/tests/test_io/test_renderers.py b/plotly/tests/test_io/test_renderers.py new file mode 100644 index 00000000000..d8ff588852b --- /dev/null +++ b/plotly/tests/test_io/test_renderers.py @@ -0,0 +1,369 @@ +import json +import sys +import base64 +import threading +import time + +import pytest +import requests + +import plotly.graph_objs as go +import plotly.io as pio +from plotly.offline import get_plotlyjs + +if sys.version_info.major == 3 and sys.version_info.minor >= 3: + import unittest.mock as mock + from unittest.mock import MagicMock +else: + import mock + from mock import MagicMock + + +# fixtures +# -------- +@pytest.fixture +def fig1(request): + return go.Figure(data=[{'type': 'scatter', + 'marker': {'color': 'green'}}], + layout={'title': {'text': 'Figure title'}}) + + +# JSON +# ---- +def test_json_renderer_mimetype(fig1): + pio.renderers.default = 'json' + expected = {'application/json': json.loads(pio.to_json(fig1))} + + bundle = fig1._repr_mimebundle_(None, None) + assert bundle == expected + + +def test_json_renderer_show(fig1): + pio.renderers.default = 'json' + expected_bundle = {'application/json': json.loads(pio.to_json(fig1))} + + with mock.patch('IPython.display.display') as mock_display: + pio.show(fig1) + + mock_display.assert_called_once_with(expected_bundle, raw=True) + + +def test_json_renderer_show_override(fig1): + pio.renderers.default = 'notebook' + expected_bundle = {'application/json': json.loads(pio.to_json(fig1))} + + with mock.patch('IPython.display.display') as mock_display: + pio.show(fig1, renderer='json') + + mock_display.assert_called_once_with(expected_bundle, raw=True) + + +# Plotly mimetype +# --------------- +plotly_mimetype = 'application/vnd.plotly.v1+json' +plotly_mimetype_renderers = [ + 'plotly_mimetype', 'jupyterlab', 'vscode', 'nteract'] + + +@pytest.mark.parametrize('renderer', plotly_mimetype_renderers) +def test_plotly_mimetype_renderer_mimetype(fig1, renderer): + pio.renderers.default = renderer + expected = {plotly_mimetype: json.loads( + pio.to_json(fig1, remove_uids=False))} + + expected[plotly_mimetype]['config'] = { + 'plotlyServerURL': 'https://plot.ly'} + + bundle = fig1._repr_mimebundle_(None, None) + assert bundle == expected + + +@pytest.mark.parametrize('renderer', plotly_mimetype_renderers) +def test_plotly_mimetype_renderer_show(fig1, renderer): + pio.renderers.default = renderer + expected = {plotly_mimetype: json.loads( + pio.to_json(fig1, remove_uids=False))} + + expected[plotly_mimetype]['config'] = { + 'plotlyServerURL': 'https://plot.ly'} + + with mock.patch('IPython.display.display') as mock_display: + pio.show(fig1) + + mock_display.assert_called_once_with(expected, raw=True) + + +# Static Image +# ------------ +def test_png_renderer_mimetype(fig1): + pio.renderers.default = 'png' + + # Configure renderer so that we can use the same parameters + # to build expected image below + pio.renderers['png'].width = 400 + pio.renderers['png'].height = 500 + pio.renderers['png'].scale = 1 + + image_bytes = pio.to_image(fig1, width=400, height=500, scale=1) + image_str = base64.b64encode(image_bytes).decode('utf8') + + expected = {'image/png': image_str} + + bundle = fig1._repr_mimebundle_(None, None) + assert bundle == expected + + +def test_svg_renderer_show(fig1): + pio.renderers.default = 'svg' + pio.renderers['svg'].width = 400 + pio.renderers['svg'].height = 500 + pio.renderers['svg'].scale = 1 + + with mock.patch('IPython.display.display') as mock_display: + pio.show(fig1) + + # Check call args. + # SVGs generated by orca are currently not reproducible so we just + # check the mime type and that the resulting string is an SVG with the + # expected size + mock_call_args = mock_display.call_args + + mock_arg1 = mock_call_args[0][0] + assert list(mock_arg1) == ['image/svg+xml'] + assert mock_arg1['image/svg+xml'].startswith( + ' Date: Tue, 19 Mar 2019 06:50:13 -0400 Subject: [PATCH 15/48] Added IFrameRenderer --- plotly/io/_base_renderers.py | 91 +++++++++++++++++++++++++++++++++++- plotly/io/_renderers.py | 3 +- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index 9f2c9332349..b17a60a1669 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -3,6 +3,7 @@ import webbrowser import uuid import inspect +import os import six from plotly.io import to_json, to_image @@ -11,6 +12,7 @@ from plotly.offline.offline import _get_jconfig, get_plotlyjs ipython_display = optional_imports.get_module('IPython.display') +IPython = optional_imports.get_module('IPython') try: from http.server import BaseHTTPRequestHandler, HTTPServer @@ -451,6 +453,93 @@ def __init__(self, config=None, auto_play=False): auto_play=auto_play) +class IFrameRenderer(MimetypeRenderer): + """ + Renderer to display interactive figures using an IFrame. HTML + representations of Figures are saved to an `iframe_figures/` directory and + iframe HTML elements that reference these files are inserted into the + notebook. + + With this approach, neither plotly.js nor the figure data are embedded in + the notebook, so this is a good choice for notebooks that contain so many + large figures that basic operations (like saving and opening) become + very slow. + + Notebooks using this renderer will display properly when exported to HTML + as long as the `iframe_figures/` directory is placed in the same directory + as the exported html file. + + Note that the HTML files in `iframe_figures/` are numbered according to + the IPython cell execution count and so they will start being overwritten + each time the kernel is restarted. This directory may be deleted whenever + the kernel is restarted and it will be automatically recreated. + + mime type: 'text/html' + """ + def __init__(self, + config=None, + auto_play=False): + + self.config = config + self.auto_play = auto_play + + def to_mimebundle(self, fig_dict): + + # Make iframe size slightly larger than figure size to avoid + # having iframe have its own scroll bar. + iframe_buffer = 20 + layout = fig_dict.get('layout', {}) + if 'width' in layout: + iframe_width = layout['width'] + iframe_buffer + else: + layout['width'] = 700 + iframe_width = layout['width'] + iframe_buffer + + if 'height' in layout: + iframe_height = layout['height'] + iframe_buffer + else: + layout['height'] = 450 + iframe_height = layout['height'] + iframe_buffer + + renderer = HtmlRenderer( + connected=False, + fullhtml=True, + requirejs=False, + global_init=False, + config=self.config, + auto_play=self.auto_play) + + bundle = renderer.to_mimebundle(fig_dict) + html = bundle['text/html'] + + # Build filename using ipython cell number + ip = IPython.get_ipython() + cell_number = list(ip.history_manager.get_tail(1))[0][1] + 1 + dirname = 'iframe_figures' + filename = '{dirname}/figure_{cell_number}.html'.format( + dirname=dirname, cell_number=cell_number) + + # Make directory for + os.makedirs(dirname, exist_ok=True) + + # Write HTML + with open(filename, 'wt') as f: + f.write(html) + + # Build IFrame + iframe_html = """\ + +""".format(width=iframe_width, height=iframe_height, src=filename) + + return {'text/html': iframe_html} + + class SideEffectRenderer(RendererRepr): """ Base class for side-effect renderers. SideEffectRenderer subclasses @@ -528,8 +617,6 @@ def __init__(self, new=0, autoraise=True): - # TODO: add browser name and open num here - # define self.config = config self.auto_play = auto_play self.using = using diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index 04511abca12..e4ecc49b72f 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -9,7 +9,7 @@ from plotly.io._base_renderers import ( MimetypeRenderer, SideEffectRenderer, PlotlyRenderer, NotebookRenderer, KaggleRenderer, ColabRenderer, JsonRenderer, PngRenderer, JpegRenderer, - SvgRenderer, PdfRenderer, BrowserRenderer) + SvgRenderer, PdfRenderer, BrowserRenderer, IFrameRenderer) from plotly.io._utils import validate_coerce_fig_to_dict ipython_display = optional_imports.get_module('IPython.display') @@ -327,6 +327,7 @@ def show(fig, renderer=None, validate=True): renderers['browser'] = BrowserRenderer(config=config) renderers['firefox'] = BrowserRenderer(config=config, using='firefox') renderers['chrome'] = BrowserRenderer(config=config, using='chrome') +renderers['iframe'] = IFrameRenderer(config=config) # Set default renderer # -------------------- From 9d082f0e2be3eff9fab5940ddbcf9f1336a98bb8 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 19 Mar 2019 12:31:17 -0400 Subject: [PATCH 16/48] Proper HTML tags for fullhtml --- plotly/io/_base_renderers.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index b17a60a1669..eb902d9c501 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -362,8 +362,13 @@ def to_mimebundle(self, fig_dict): # Handle fullhtml if self.fullhtml: - html_start = '' - html_end = '' + html_start = """\ + + +""" + html_end = """\ + +""" else: html_start = '' html_end = '' From b499b35da68a56f1197af8a6728f9704c7a6d2e9 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 19 Mar 2019 17:26:40 -0400 Subject: [PATCH 17/48] Added initial to_html functions to plotly.io --- plotly/io/__init__.py | 2 + plotly/io/_html.py | 319 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 321 insertions(+) create mode 100644 plotly/io/_html.py diff --git a/plotly/io/__init__.py b/plotly/io/__init__.py index 063346ec9c9..119436755d7 100644 --- a/plotly/io/__init__.py +++ b/plotly/io/__init__.py @@ -8,3 +8,5 @@ from ._renderers import renderers, show from . import base_renderers + +from ._html import to_div, to_html, write_html diff --git a/plotly/io/_html.py b/plotly/io/_html.py new file mode 100644 index 00000000000..0e7a5933b54 --- /dev/null +++ b/plotly/io/_html.py @@ -0,0 +1,319 @@ +import uuid +import json +import os +import webbrowser + +import six + +from plotly.io._utils import validate_coerce_fig_to_dict +from plotly.offline.offline import _get_jconfig, get_plotlyjs +from plotly import utils + + +# Build script to set global PlotlyConfig object. This must execute before +# plotly.js is loaded. +_window_plotly_config = """\ +window.PlotlyConfig = {MathJaxConfig: 'local'};""" + + +def to_div(fig, + config=None, + auto_play=True, + include_plotlyjs=True, + include_mathjax=False, + validate=True): + """ + Convert a figure to an HTML div representation + + Parameters + ---------- + fig: + Figure object or dict representing a figure + config: dict or None (default None) + Plotly.js figure config options + auto_play: bool (default=True) + Whether to automatically start the animation sequence on page load + if the figure contains frames. Has no effect if the figure does not + contain frames. + include_plotlyjs: bool or string (default True) + Specifies how the plotly.js library is included/loaded in the output + 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 'require', Plotly.js is loaded using require.js. This option + assumes that require.js is globally available and that it has been + globally configured to know how to find Plotly.js as 'plotly'. + + If a string that ends in '.js', a script tag is included that + references the specified path. This approach can be used to point + the resulting HTML file to an alternative CDN or local bundle. + + If False, no script tag referencing plotly.js is included. This is + useful when the resulting div string will be placed inside an HTML + document that already loads plotly.js. + include_mathjax: bool or string (default False) + Specifies how the MathJax.js library is included in the output html + div string. MathJax is required in order to display labels + with LaTeX typesetting. + + If False, no script tag referencing MathJax.js will be included in the + output. + + If 'cdn', a script tag that references a MathJax CDN location will be + included in the output. HTML div strings generated with this option + will be able to display LaTeX typesetting as long as internet access + is available. + + If a string that ends in '.js', a script tag is included that + references the specified path. This approach can be used to point the + resulting HTML div string to an alternative CDN. + validate: bool (default True) + True if the figure should be validated before being converted to + JSON, False otherwise. + Returns + ------- + str + Representation of figure as an HTML div string + """ + # ## Validate figure ## + fig_dict = validate_coerce_fig_to_dict(fig, validate) + + # ## Generate div id ## + plotdivid = uuid.uuid4() + + # ## Serialize figure ## + jdata = json.dumps( + fig_dict.get('data', []), cls=utils.PlotlyJSONEncoder) + jlayout = json.dumps( + fig_dict.get('layout', {}), cls=utils.PlotlyJSONEncoder) + + if fig_dict.get('frames', None): + jframes = json.dumps( + fig_dict.get('frames', []), cls=utils.PlotlyJSONEncoder) + else: + jframes = None + + # ## Serialize figure config ## + config = _get_jconfig(config) + jconfig = json.dumps(config) + + # ## Get platform URL ## + plotly_platform_url = config.get('plotly_domain', 'https://plot.ly') + + # ## Build script body ## + # This is the part that actually calls Plotly.js + if jframes: + if auto_play: + animate = """.then(function(){{ + Plotly.animate('{id}'); + }}""".format(id=plotdivid) + + else: + animate = '' + + script = ''' + if (document.getElementById("{id}")) {{ + Plotly.plot( + '{id}', + {data}, + {layout}, + {config} + ).then(function () {add_frames}){animate} + }} + '''.format( + id=plotdivid, + data=jdata, + layout=jlayout, + config=jconfig, + add_frames="{" + "return Plotly.addFrames('{id}',{frames}".format( + id=plotdivid, frames=jframes + ) + ");}", + animate=animate + ) + else: + script = """ + if (document.getElementById("{id}")) {{ + Plotly.newPlot("{id}", {data}, {layout}, {config}); + }} + """.format( + id=plotdivid, + data=jdata, + layout=jlayout, + config=jconfig) + + # ## Handle loading/initializing plotly.js ## + include_plotlyjs_orig = include_plotlyjs + if isinstance(include_plotlyjs, six.string_types): + include_plotlyjs = include_plotlyjs.lower() + + # Start/end of requirejs block (if any) + require_start = '' + require_end = '' + + # Init and load + load_plotlyjs = '' + + # Init plotlyjs. This block needs to run before plotly.js is loaded in + # order for MathJax configuration to work properly + init_plotlyjs = """"""" + + if include_plotlyjs == 'require': + require_start = 'require(["plotly"], function(Plotly) {' + require_end = '});' + + elif include_plotlyjs == 'cdn': + load_plotlyjs = """\ + {init_plotlyjs} + \ +""".format(init_plotlyjs=init_plotlyjs) + + elif include_plotlyjs == 'directory': + load_plotlyjs = """\ + {init_plotlyjs} + \ +""".format(init_plotlyjs=init_plotlyjs) + + elif (isinstance(include_plotlyjs, six.string_types) and + include_plotlyjs.endswith('.js')): + load_plotlyjs = """\ + {init_plotlyjs} + \ +""".format(init_plotlyjs=init_plotlyjs, + url=include_plotlyjs_orig) + + elif include_plotlyjs: + load_plotlyjs = """\ + {init_plotlyjs} + \ +""".format(init_plotlyjs=init_plotlyjs, + plotlyjs=get_plotlyjs()) + + # ## Handle loading/initializing MathJax ## + include_mathjax_orig = include_mathjax + if isinstance(include_mathjax, six.string_types): + include_mathjax = include_mathjax.lower() + + mathjax_template = """\ +""" + + if include_mathjax == 'cdn': + mathjax_script = mathjax_template.format( + url=('https://cdnjs.cloudflare.com' + '/ajax/libs/mathjax/2.7.5/MathJax.js')) + + elif (isinstance(include_mathjax, six.string_types) and + include_mathjax.endswith('.js')): + + mathjax_script = mathjax_template.format( + url=include_mathjax_orig) + elif not include_mathjax: + mathjax_script = '' + else: + raise ValueError("""\ +Invalid value of type {typ} received as the include_mathjax argument + Received value: {val} + +include_mathjax may be specified as False, 'cdn', or a string ending with '.js' +""".format(typ=type(include_mathjax), val=repr(include_mathjax))) + + plotly_html_div = """\ +
+ {mathjax_script} + {load_plotlyjs} +
+ +
""".format( + mathjax_script=mathjax_script, + load_plotlyjs=load_plotlyjs, + id=plotdivid, + plotly_platform_url=plotly_platform_url, + require_start=require_start, + script=script, + require_end=require_end) + + return plotly_html_div + + +def to_html(fig, + config=None, + auto_play=True, + include_plotlyjs=True, + include_mathjax=False, + validate=True): + + div = to_div(fig, + config=config, + auto_play=auto_play, + include_plotlyjs=include_plotlyjs, + include_mathjax=include_mathjax, + validate=validate) + return """\ + + + + {div} + +""".format(div=div) + + +def write_html(fig, + file, + config=None, + auto_play=True, + include_plotlyjs=True, + include_mathjax=False, + validate=True, + auto_open=False): + + # Build HTML string + html_str = to_html( + fig, + config=config, + auto_play=auto_play, + include_plotlyjs=include_plotlyjs, + include_mathjax=include_mathjax, + validate=validate) + + # Check if file is a string + file_is_str = isinstance(file, six.string_types) + + # Write HTML string + if file_is_str: + with open(file, 'w') as f: + f.write(html_str) + else: + file.write(html_str) + + # Check if we should copy plotly.min.js to output directory + if file_is_str and include_plotlyjs == 'directory': + bundle_path = os.path.join( + os.path.dirname(file), 'plotly.min.js') + + if not os.path.exists(bundle_path): + with open(bundle_path, 'w') as f: + f.write(get_plotlyjs()) + + # Handle auto_open + if file_is_str and auto_open: + url = 'file://' + os.path.abspath(file) + webbrowser.open(url) From 63424a5fd26be789c5cf7ffa2c2c1c2d9d1d1e38 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 19 Mar 2019 18:00:56 -0400 Subject: [PATCH 18/48] Reimplement plotly.offline.plot using plotly.io.write_html/to_div. Updated tests only slightly to reflect the change in how resizing is handled now. Before there was a custom callback resize script, now we rely on the plotly.js 'responsive' config option --- plotly/io/_html.py | 31 ++-- plotly/offline/offline.py | 153 ++++-------------- .../test_core/test_offline/test_offline.py | 5 +- 3 files changed, 51 insertions(+), 138 deletions(-) diff --git a/plotly/io/_html.py b/plotly/io/_html.py index 0e7a5933b54..57f628afa94 100644 --- a/plotly/io/_html.py +++ b/plotly/io/_html.py @@ -13,8 +13,14 @@ # Build script to set global PlotlyConfig object. This must execute before # plotly.js is loaded. _window_plotly_config = """\ -window.PlotlyConfig = {MathJaxConfig: 'local'};""" +""" +_mathjax_config = """\ +""" def to_div(fig, config=None, @@ -168,38 +174,35 @@ def to_div(fig, # Init plotlyjs. This block needs to run before plotly.js is loaded in # order for MathJax configuration to work properly - init_plotlyjs = """"""" - if include_plotlyjs == 'require': require_start = 'require(["plotly"], function(Plotly) {' require_end = '});' elif include_plotlyjs == 'cdn': load_plotlyjs = """\ - {init_plotlyjs} + {win_config} \ -""".format(init_plotlyjs=init_plotlyjs) +""".format(win_config=_window_plotly_config) elif include_plotlyjs == 'directory': load_plotlyjs = """\ - {init_plotlyjs} + {win_config} \ -""".format(init_plotlyjs=init_plotlyjs) +""".format(win_config=_window_plotly_config) elif (isinstance(include_plotlyjs, six.string_types) and include_plotlyjs.endswith('.js')): load_plotlyjs = """\ - {init_plotlyjs} + {win_config} \ -""".format(init_plotlyjs=init_plotlyjs, +""".format(win_config=_window_plotly_config, url=include_plotlyjs_orig) elif include_plotlyjs: load_plotlyjs = """\ - {init_plotlyjs} + {win_config} \ -""".format(init_plotlyjs=init_plotlyjs, +""".format(win_config=_window_plotly_config, plotlyjs=get_plotlyjs()) # ## Handle loading/initializing MathJax ## @@ -213,13 +216,13 @@ def to_div(fig, if include_mathjax == 'cdn': mathjax_script = mathjax_template.format( url=('https://cdnjs.cloudflare.com' - '/ajax/libs/mathjax/2.7.5/MathJax.js')) + '/ajax/libs/mathjax/2.7.5/MathJax.js')) + _mathjax_config elif (isinstance(include_mathjax, six.string_types) and include_mathjax.endswith('.js')): mathjax_script = mathjax_template.format( - url=include_mathjax_orig) + url=include_mathjax_orig) + _mathjax_config elif not include_mathjax: mathjax_script = '' else: diff --git a/plotly/offline/offline.py b/plotly/offline/offline.py index 1180a897c1d..4be6f961d99 100644 --- a/plotly/offline/offline.py +++ b/plotly/offline/offline.py @@ -530,7 +530,7 @@ def iplot(figure_or_data, show_link=False, link_text='Export to plot.ly', def plot(figure_or_data, show_link=False, link_text='Export to plot.ly', validate=True, output_type='file', include_plotlyjs=True, filename='temp-plot.html', auto_open=True, image=None, - image_filename='plot_image', image_width=800, image_height=600, + image_filename=None, image_width=None, image_height=None, config=None, include_mathjax=False, auto_play=True): """ Create a plotly graph locally as an HTML document or string. @@ -612,16 +612,6 @@ def plot(figure_or_data, show_link=False, link_text='Export to plot.ly', auto_open (default=True) -- If True, open the saved file in a web browser after saving. This argument only applies if `output_type` is 'file'. - image (default=None |'png' |'jpeg' |'svg' |'webp') -- This parameter sets - the format of the image to be downloaded, if we choose to download an - image. This parameter has a default value of None indicating that no - image should be downloaded. Please note: for higher resolution images - and more export options, consider making requests to our image servers. - Type: `help(py.image)` for more details. - image_filename (default='plot_image') -- Sets the name of the file your - image will be saved to. The extension should not be included. - image_height (default=600) -- Specifies the height of the image in `px`. - image_width (default=800) -- Specifies the width of the image in `px`. config (default=None) -- Plot view options dictionary. Keyword arguments `show_link` and `link_text` set the associated options in this dictionary if it doesn't contain them already. @@ -645,6 +635,9 @@ def plot(figure_or_data, show_link=False, link_text='Export to plot.ly', sequence on page load if the figure contains frames. Has no effect if the figure does not contain frames. """ + import plotly.io as pio + + # Output type if output_type not in ['div', 'file']: raise ValueError( "`output_type` argument must be 'div' or 'file'. " @@ -655,123 +648,43 @@ def plot(figure_or_data, show_link=False, link_text='Export to plot.ly', "Adding .html to the end of your file.") filename += '.html' + # Deprecations + if image: + warnings.warn(""" +Image export using plotly.offline.plot is no longer supported. + Please use plotly.io.write_image instead""", DeprecationWarning) + + # Config config = dict(config) if config else {} config.setdefault('showLink', show_link) config.setdefault('linkText', link_text) - plot_html, plotdivid, width, height = _plot_html( - figure_or_data, config, validate, - '100%', '100%', global_requirejs=False, auto_play=auto_play) + figure = tools.return_figure_from_figure_or_data(figure_or_data, validate) + width = figure.get('layout', {}).get('width', '100%') + height = figure.get('layout', {}).get('height', '100%') - # Build resize_script - resize_script = '' if width == '100%' or height == '100%': - resize_script = _build_resize_script(plotdivid) - - # Process include_plotlyjs and build plotly_js_script - include_plotlyjs_orig = include_plotlyjs - if isinstance(include_plotlyjs, six.string_types): - include_plotlyjs = include_plotlyjs.lower() - - if include_plotlyjs == 'cdn': - plotly_js_script = _window_plotly_config + """\ -""" - elif include_plotlyjs == 'directory': - plotly_js_script = (_window_plotly_config + - '') - elif (isinstance(include_plotlyjs, six.string_types) and - include_plotlyjs.endswith('.js')): - plotly_js_script = (_window_plotly_config + - ''.format( - url=include_plotlyjs_orig)) - elif include_plotlyjs: - plotly_js_script = ''.join([ - _window_plotly_config, - '', - ]) - else: - plotly_js_script = '' - - # Process include_mathjax and build mathjax_script - include_mathjax_orig = include_mathjax - if isinstance(include_mathjax, six.string_types): - include_mathjax = include_mathjax.lower() - - if include_mathjax == 'cdn': - mathjax_script = _build_mathjax_script( - url=('https://cdnjs.cloudflare.com' - '/ajax/libs/mathjax/2.7.5/MathJax.js')) + _mathjax_config - elif (isinstance(include_mathjax, six.string_types) and - include_mathjax.endswith('.js')): - mathjax_script = _build_mathjax_script( - url=include_mathjax_orig) + _mathjax_config - elif not include_mathjax: - mathjax_script = '' - else: - raise ValueError("""\ -Invalid value of type {typ} received as the include_mathjax argument - Received value: {val} - -include_mathjax may be specified as False, 'cdn', or a string ending with '.js' -""".format(typ=type(include_mathjax), val=repr(include_mathjax))) + config.setdefault('responsive', True) if output_type == 'file': - with open(filename, 'w') as f: - if image: - if image not in __IMAGE_FORMATS: - raise ValueError('The image parameter must be one of the ' - 'following: {}'.format(__IMAGE_FORMATS) - ) - # if the check passes then download script is injected. - # write the download script: - script = get_image_download_script('plot') - script = script.format(format=image, - width=image_width, - height=image_height, - filename=image_filename, - plot_id=plotdivid) - else: - script = '' - - f.write(''.join([ - '', - '', - '', - mathjax_script, - plotly_js_script, - plot_html, - resize_script, - script, - '', - ''])) - - # 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) - - return url - - elif output_type == 'div': - - return ''.join([ - '
', - mathjax_script, - plotly_js_script, - plot_html, - resize_script, - '
', - ]) + pio.write_html( + figure, + filename, + config=config, + auto_play=auto_play, + include_plotlyjs=include_plotlyjs, + include_mathjax=include_mathjax, + validate=validate, + auto_open=auto_open) + return filename + else: + return pio.to_div( + figure, + config=config, + auto_play=auto_play, + include_plotlyjs=include_plotlyjs, + include_mathjax=include_mathjax, + validate=validate) def plot_mpl(mpl_fig, resize=False, strip_style=False, diff --git a/plotly/tests/test_core/test_offline/test_offline.py b/plotly/tests/test_core/test_offline/test_offline.py index 8fb3499180c..123b263fcdd 100644 --- a/plotly/tests/test_core/test_offline/test_offline.py +++ b/plotly/tests/test_core/test_offline/test_offline.py @@ -36,10 +36,7 @@ } -resize_code_strings = [ - 'window.addEventListener("resize", ', - 'Plotly.Plots.resize(' - ] +resize_code_strings = ['"responsive": true'] PLOTLYJS = plotly.offline.get_plotlyjs() From 28463c236bb381bf25d86c05b5e59db14ba7abe1 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 19 Mar 2019 18:53:29 -0400 Subject: [PATCH 19/48] Reimplement plotly.offline.iplot and init_notebook_mode pio.show --- plotly/io/_base_renderers.py | 4 ++ plotly/io/_renderers.py | 29 ++++++-- plotly/offline/offline.py | 126 +++++------------------------------ 3 files changed, 43 insertions(+), 116 deletions(-) diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index eb902d9c501..caf3f61fbec 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -40,6 +40,10 @@ def __repr__(self): for k in init_args), doc=self.__doc__) + def __hash__(self): + # Constructor args fully define uniqueness + return hash(repr(self)) + class MimetypeRenderer(RendererRepr): """ diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index e4ecc49b72f..4ef7aa170ed 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -1,6 +1,8 @@ from __future__ import absolute_import, division import textwrap +from copy import copy + import six import os @@ -111,8 +113,10 @@ def default(self): @default.setter def default(self, value): # Handle None - if value is None: - self._default_name = None + if not value: + # _default_name should always be a string so we can do + # pio.renderers.default.split('+') + self._default_name = '' self._default_renderers = [] return @@ -174,7 +178,7 @@ def _available_templates_str(self): )) return available - def _build_mime_bundle(self, fig_dict, renderers_string=None): + def _build_mime_bundle(self, fig_dict, renderers_string=None, **kwargs): """ Build a mime bundle dict containing a kev/value pair for each MimetypeRenderer specified in either the default renderer string, @@ -207,11 +211,17 @@ def _build_mime_bundle(self, fig_dict, renderers_string=None): bundle = {} for renderer in renderers_list: if isinstance(renderer, MimetypeRenderer): + renderer = copy(renderer) + for k, v in kwargs.items(): + if hasattr(renderer, k): + setattr(renderer, k, v) + bundle.update(renderer.to_mimebundle(fig_dict)) return bundle - def _perform_side_effect_rendering(self, fig_dict, renderers_string=None): + def _perform_side_effect_rendering( + self, fig_dict, renderers_string=None, **kwargs): """ Perform side-effect rendering for each SideEffectRenderer specified in either the default renderer string, or in the supplied @@ -243,6 +253,11 @@ def _perform_side_effect_rendering(self, fig_dict, renderers_string=None): for renderer in renderers_list: if isinstance(renderer, SideEffectRenderer): + renderer = copy(renderer) + for k, v in kwargs.items(): + if hasattr(renderer, k): + setattr(renderer, k, v) + renderer.render(fig_dict) @@ -253,7 +268,7 @@ def _perform_side_effect_rendering(self, fig_dict, renderers_string=None): # Show -def show(fig, renderer=None, validate=True): +def show(fig, renderer=None, validate=True, **kwargs): """ Show a figure using either the default renderer(s) or the renderer(s) specified by the renderer argument @@ -280,7 +295,7 @@ def show(fig, renderer=None, validate=True): # Mimetype renderers bundle = renderers._build_mime_bundle( - fig_dict, renderers_string=renderer) + fig_dict, renderers_string=renderer, **kwargs) if bundle: if not ipython_display: raise ValueError( @@ -290,7 +305,7 @@ def show(fig, renderer=None, validate=True): # Side effect renderers renderers._perform_side_effect_rendering( - fig_dict, renderers_string=renderer) + fig_dict, renderers_string=renderer, **kwargs) # Register renderers diff --git a/plotly/offline/offline.py b/plotly/offline/offline.py index 4be6f961d99..19c33c6a3a3 100644 --- a/plotly/offline/offline.py +++ b/plotly/offline/offline.py @@ -24,8 +24,6 @@ ipython_display = optional_imports.get_module('IPython.display') matplotlib = optional_imports.get_module('matplotlib') -__PLOTLY_OFFLINE_INITIALIZED = False - __IMAGE_FORMATS = ['jpeg', 'png', 'webp', 'svg'] @@ -262,53 +260,15 @@ def init_notebook_mode(connected=False): your notebook, resulting in much larger notebook sizes compared to the case where `connected=True`. """ + import plotly.io as pio + if not ipython: raise ImportError('`iplot` can only run inside an IPython Notebook.') - global __PLOTLY_OFFLINE_INITIALIZED - if connected: - # Inject plotly.js into the output cell - script_inject = ( - '{win_config}' - '{mathjax_config}' - '' - ).format(win_config=_window_plotly_config, - mathjax_config=_mathjax_config) + pio.renderers.default = 'notebook_connected+plotly_mimetype' else: - # Inject plotly.js into the output cell - script_inject = ( - '{win_config}' - '{mathjax_config}' - '' - '').format(script=get_plotlyjs(), - win_config=_window_plotly_config, - mathjax_config=_mathjax_config) - - display_bundle = { - 'text/html': script_inject, - 'text/vnd.plotly.v1+html': script_inject - } - ipython_display.display(display_bundle, raw=True) - __PLOTLY_OFFLINE_INITIALIZED = True + pio.renderers.default = 'notebook+plotly_mimetype' def _plot_html(figure_or_data, config, validate, default_width, @@ -410,8 +370,8 @@ def _plot_html(figure_or_data, config, validate, default_width, def iplot(figure_or_data, show_link=False, link_text='Export to plot.ly', - validate=True, image=None, filename='plot_image', image_width=800, - image_height=600, config=None, auto_play=True): + validate=True, image=None, filename=None, image_width=None, + image_height=None, config=None, auto_play=True): """ Draw plotly graphs inside an IPython or Jupyter notebook without connecting to an external server. @@ -434,16 +394,8 @@ def iplot(figure_or_data, show_link=False, link_text='Export to plot.ly', has become outdated with your version of graph_reference.json or if you need to include extra, unnecessary keys in your figure. - image (default=None |'png' |'jpeg' |'svg' |'webp') -- This parameter sets - the format of the image to be downloaded, if we choose to download an - image. This parameter has a default value of None indicating that no - image should be downloaded. Please note: for higher resolution images - and more export options, consider making requests to our image servers. - Type: `help(py.image)` for more details. filename (default='plot') -- Sets the name of the file your image will be saved to. The extension should not be included. - image_height (default=600) -- Specifies the height of the image in `px`. - image_width (default=800) -- Specifies the width of the image in `px`. config (default=None) -- Plot view options dictionary. Keyword arguments `show_link` and `link_text` set the associated options in this dictionary if it doesn't contain them already. @@ -461,70 +413,26 @@ def iplot(figure_or_data, show_link=False, link_text='Export to plot.ly', iplot([{'x': [1, 2, 3], 'y': [5, 2, 7]}], image='png') ``` """ + import plotly.io as pio + if not ipython: raise ImportError('`iplot` can only run inside an IPython Notebook.') + # Deprecations + if image: + warnings.warn(""" + Image export using plotly.offline.plot is no longer supported. + Please use plotly.io.write_image instead""", DeprecationWarning) + config = dict(config) if config else {} config.setdefault('showLink', show_link) config.setdefault('linkText', link_text) - jconfig = _get_jconfig(config) - + # Get figure figure = tools.return_figure_from_figure_or_data(figure_or_data, validate) - # Though it can add quite a bit to the display-bundle size, we include - # multiple representations of the plot so that the display environment can - # choose which one to act on. - data = _json.loads(_json.dumps(figure['data'], - cls=plotly.utils.PlotlyJSONEncoder)) - layout = _json.loads(_json.dumps(figure.get('layout', {}), - cls=plotly.utils.PlotlyJSONEncoder)) - frames = _json.loads(_json.dumps(figure.get('frames', None), - cls=plotly.utils.PlotlyJSONEncoder)) - - fig = {'data': data, 'layout': layout, 'config': jconfig} - if frames: - fig['frames'] = frames - - display_bundle = {'application/vnd.plotly.v1+json': fig} - - if __PLOTLY_OFFLINE_INITIALIZED: - plot_html, plotdivid, width, height = _plot_html( - figure_or_data, config, validate, '100%', 525, True, auto_play) - resize_script = '' - if width == '100%' or height == '100%': - resize_script = _build_resize_script( - plotdivid, 'window._Plotly') - - display_bundle['text/html'] = plot_html + resize_script - display_bundle['text/vnd.plotly.v1+html'] = plot_html + resize_script - - ipython_display.display(display_bundle, raw=True) - - if image: - if not __PLOTLY_OFFLINE_INITIALIZED: - raise PlotlyError('\n'.join([ - 'Plotly Offline mode has not been initialized in this notebook. ' - 'Run: ', - '', - 'import plotly', - 'plotly.offline.init_notebook_mode() ' - '# run at the start of every ipython notebook', - ])) - if image not in __IMAGE_FORMATS: - raise ValueError('The image parameter must be one of the following' - ': {}'.format(__IMAGE_FORMATS) - ) - # if image is given, and is a valid format, we will download the image - script = get_image_download_script('iplot').format(format=image, - width=image_width, - height=image_height, - filename=filename, - plot_id=plotdivid) - # allow time for the plot to draw - time.sleep(1) - # inject code to download an image of the plot - ipython_display.display(ipython_display.HTML(script)) + # Show figure + pio.show(figure, validate=validate, config=config, auto_play=auto_play) def plot(figure_or_data, show_link=False, link_text='Export to plot.ly', From e4421bb8d6d97257cf45ef77494576d9b18c70d7 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 19 Mar 2019 19:45:14 -0400 Subject: [PATCH 20/48] Add responsive=True config when figure width/height aren't set --- plotly/io/_html.py | 6 ++++++ plotly/io/_renderers.py | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/plotly/io/_html.py b/plotly/io/_html.py index 57f628afa94..e0da100e891 100644 --- a/plotly/io/_html.py +++ b/plotly/io/_html.py @@ -114,6 +114,12 @@ def to_div(fig, # ## Serialize figure config ## config = _get_jconfig(config) + + # Check whether we should add responsive + layout_dict = fig_dict.get('layout', {}) + if layout_dict.get('width', None) is None: + config.setdefault('responsive', True) + jconfig = json.dumps(config) # ## Get platform URL ## diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index 4ef7aa170ed..41f80f2afe8 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -319,7 +319,7 @@ def show(fig, renderer=None, validate=True, **kwargs): renderers['vscode'] = plotly_renderer # HTML-based -config = {'responsive': True} +config = {} renderers['notebook'] = NotebookRenderer(config=config) renderers['notebook_connected'] = NotebookRenderer( config=config, connected=True) From 3b11783184691dc3117d06c631924d2f2ba96a47 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 19 Mar 2019 19:46:59 -0400 Subject: [PATCH 21/48] Add MathJax configuration when initializing HTML renderer in notebook --- plotly/io/_base_renderers.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index caf3f61fbec..b0106397f2c 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -208,6 +208,8 @@ def __init__(self, width=None, height=None, scale=None): _window_plotly_config = """\ window.PlotlyConfig = {MathJaxConfig: 'local'};""" +_mathjax_config = """\ +if (window.MathJax) {MathJax.Hub.Config({SVG: {font: "STIX-Web"}});}""" class HtmlRenderer(MimetypeRenderer): """ @@ -246,6 +248,7 @@ def activate(self): script = """\ - """.format(win_config=_window_plotly_config) + """.format(win_config=_window_plotly_config, + mathjax_config=_mathjax_config) else: # If not connected then we embed a copy of the plotly.js @@ -266,6 +270,7 @@ def activate(self): script = """\ - """.format(script=get_plotlyjs(), win_config=_window_plotly_config) + """.format(script=get_plotlyjs(), + win_config=_window_plotly_config, + mathjax_config=_mathjax_config) ipython_display.display_html(script, raw=True) From 258079776c45fb0c7bfc5010d68307aa62a7c0a5 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 19 Mar 2019 19:48:38 -0400 Subject: [PATCH 22/48] Reimplement HTML Renderers using the plotly.io html functions Use include_plotlyjs='directory' for iframe renderer. --- plotly/io/_base_renderers.py | 155 +++++++---------------------------- 1 file changed, 31 insertions(+), 124 deletions(-) diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index b0106397f2c..6d40df98d9a 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -211,6 +211,7 @@ def __init__(self, width=None, height=None, scale=None): _mathjax_config = """\ if (window.MathJax) {MathJax.Hub.Config({SVG: {font: "STIX-Web"}});}""" + class HtmlRenderer(MimetypeRenderer): """ Base class for all HTML mime type renderers @@ -288,124 +289,35 @@ def activate(self): ipython_display.display_html(script, raw=True) def to_mimebundle(self, fig_dict): - plotdivid = uuid.uuid4() - - # Serialize figure - jdata = json.dumps( - fig_dict.get('data', []), cls=utils.PlotlyJSONEncoder) - jlayout = json.dumps( - fig_dict.get('layout', {}), cls=utils.PlotlyJSONEncoder) - - if fig_dict.get('frames', None): - jframes = json.dumps( - fig_dict.get('frames', []), cls=utils.PlotlyJSONEncoder) - else: - jframes = None - - # Serialize figure config - config = _get_jconfig(self.config) - jconfig = json.dumps(config) - - # Platform URL - plotly_platform_url = config.get('plotly_domain', 'https://plot.ly') - # Build script body - if jframes: - if self.auto_play: - animate = """.then(function(){{ - Plotly.animate('{id}'); - }}""".format(id=plotdivid) + from plotly.io import to_div, to_html - else: - animate = '' - - script = ''' - if (document.getElementById("{id}")) {{ - Plotly.plot( - '{id}', - {data}, - {layout}, - {config} - ).then(function () {add_frames}){animate} - }} - '''.format( - id=plotdivid, - data=jdata, - layout=jlayout, - config=jconfig, - add_frames="{" + "return Plotly.addFrames('{id}',{frames}".format( - id=plotdivid, frames=jframes - ) + ");}", - animate=animate - ) - else: - script = """ - if (document.getElementById("{id}")) {{ - Plotly.newPlot("{id}", {data}, {layout}, {config}); - }} - """.format( - id=plotdivid, - data=jdata, - layout=jlayout, - config=jconfig) - - # Build div - - # Handle require if self.requirejs: - require_start = 'require(["plotly"], function(Plotly) {' - require_end = '});' - load_plotlyjs = '' + include_plotlyjs = 'require' + include_mathjax = False + elif self.connected: + include_plotlyjs = 'cdn' + include_mathjax = 'cdn' else: - require_start = '' - require_end = '' - if self.connected: - load_plotlyjs = """\ - -""".format( - win_config=_window_plotly_config) - else: - load_plotlyjs = """\ - -""".format(win_config=_window_plotly_config, plotlyjs=get_plotlyjs()) + include_plotlyjs = True + include_mathjax = 'cdn' - # Handle fullhtml if self.fullhtml: - html_start = """\ - - -""" - html_end = """\ - -""" + html = to_html( + fig_dict, + config=self.config, + auto_play=self.auto_play, + include_plotlyjs=include_plotlyjs, + include_mathjax=include_mathjax) else: - html_start = '' - html_end = '' - - plotly_html_div = """\ -{html_start} - {load_plotlyjs} -
- -{html_end}""".format( - html_start=html_start, - load_plotlyjs=load_plotlyjs, - id=plotdivid, - plotly_platform_url=plotly_platform_url, - require_start=require_start, - script=script, - require_end=require_end, - html_end=html_end) + html = to_div( + fig_dict, + config=self.config, + auto_play=self.auto_play, + include_plotlyjs=include_plotlyjs, + include_mathjax=include_mathjax) - return {'text/html': plotly_html_div} + return {'text/html': html} class NotebookRenderer(HtmlRenderer): @@ -500,6 +412,7 @@ def __init__(self, self.auto_play = auto_play def to_mimebundle(self, fig_dict): + from plotly.io import write_html # Make iframe size slightly larger than figure size to avoid # having iframe have its own scroll bar. @@ -517,17 +430,6 @@ def to_mimebundle(self, fig_dict): layout['height'] = 450 iframe_height = layout['height'] + iframe_buffer - renderer = HtmlRenderer( - connected=False, - fullhtml=True, - requirejs=False, - global_init=False, - config=self.config, - auto_play=self.auto_play) - - bundle = renderer.to_mimebundle(fig_dict) - html = bundle['text/html'] - # Build filename using ipython cell number ip = IPython.get_ipython() cell_number = list(ip.history_manager.get_tail(1))[0][1] + 1 @@ -538,9 +440,14 @@ def to_mimebundle(self, fig_dict): # Make directory for os.makedirs(dirname, exist_ok=True) - # Write HTML - with open(filename, 'wt') as f: - f.write(html) + write_html(fig_dict, + filename, + config=self.config, + auto_play=self.auto_play, + include_plotlyjs='directory', + include_mathjax='cdn', + auto_open=False, + validate=False) # Build IFrame iframe_html = """\ From 8a787fd122cf564d7793cfb48de04cc7074bde68 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 20 Mar 2019 08:30:37 -0400 Subject: [PATCH 23/48] Restore plot/iplot image export by adding support for custom JS snippets Now there is a post_script argument to the HTML functions that allows a user to specify some custom JavaScript to run after plot creation. This can be used by plot/iplot to implement the previous save image behavior without needing to add explicit save image options to the HTML functions. --- plotly/io/_base_renderers.py | 41 +++++++++++++------ plotly/io/_html.py | 45 +++++++++++++++------ plotly/offline/offline.py | 76 ++++++++++++++++++++++++++++++------ 3 files changed, 127 insertions(+), 35 deletions(-) diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index 6d40df98d9a..447b52bd42b 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -224,7 +224,8 @@ def __init__(self, requirejs=True, global_init=False, config=None, - auto_play=False): + auto_play=False, + post_script=None): self.config = dict(config) if config else {} self.auto_play = auto_play @@ -232,6 +233,7 @@ def __init__(self, self.global_init = global_init self.requirejs = requirejs self.fullhtml = fullhtml + self.post_script = post_script def activate(self): if self.global_init: @@ -308,14 +310,16 @@ def to_mimebundle(self, fig_dict): config=self.config, auto_play=self.auto_play, include_plotlyjs=include_plotlyjs, - include_mathjax=include_mathjax) + include_mathjax=include_mathjax, + post_script=self.post_script) else: html = to_div( fig_dict, config=self.config, auto_play=self.auto_play, include_plotlyjs=include_plotlyjs, - include_mathjax=include_mathjax) + include_mathjax=include_mathjax, + post_script=self.post_script) return {'text/html': html} @@ -332,14 +336,19 @@ class NotebookRenderer(HtmlRenderer): mime type: 'text/html' """ - def __init__(self, connected=False, config=None, auto_play=False): + def __init__(self, + connected=False, + config=None, + auto_play=False, + post_script=None): super(NotebookRenderer, self).__init__( connected=connected, fullhtml=False, requirejs=True, global_init=True, config=config, - auto_play=auto_play) + auto_play=auto_play, + post_script=post_script) class KaggleRenderer(HtmlRenderer): @@ -353,14 +362,15 @@ class KaggleRenderer(HtmlRenderer): mime type: 'text/html' """ - def __init__(self, config=None, auto_play=False): + def __init__(self, config=None, auto_play=False, post_script=None): super(KaggleRenderer, self).__init__( connected=True, fullhtml=False, requirejs=True, global_init=True, config=config, - auto_play=auto_play) + auto_play=auto_play, + post_script=post_script) class ColabRenderer(HtmlRenderer): @@ -371,14 +381,15 @@ class ColabRenderer(HtmlRenderer): mime type: 'text/html' """ - def __init__(self, config=None, auto_play=False): + def __init__(self, config=None, auto_play=False, post_script=None): super(ColabRenderer, self).__init__( connected=True, fullhtml=True, requirejs=False, global_init=False, config=config, - auto_play=auto_play) + auto_play=auto_play, + post_script=post_script) class IFrameRenderer(MimetypeRenderer): @@ -406,10 +417,12 @@ class IFrameRenderer(MimetypeRenderer): """ def __init__(self, config=None, - auto_play=False): + auto_play=False, + post_script=None): self.config = config self.auto_play = auto_play + self.post_script = post_script def to_mimebundle(self, fig_dict): from plotly.io import write_html @@ -447,6 +460,7 @@ def to_mimebundle(self, fig_dict): include_plotlyjs='directory', include_mathjax='cdn', auto_open=False, + post_script=self.post_script, validate=False) # Build IFrame @@ -538,13 +552,15 @@ def __init__(self, auto_play=False, using=None, new=0, - autoraise=True): + autoraise=True, + post_script=None): self.config = config self.auto_play = auto_play self.using = using self.new = new self.autoraise = autoraise + self.post_script = post_script def render(self, fig_dict): renderer = HtmlRenderer( @@ -553,7 +569,8 @@ def render(self, fig_dict): requirejs=False, global_init=False, config=self.config, - auto_play=self.auto_play) + auto_play=self.auto_play, + post_script=self.post_script) bundle = renderer.to_mimebundle(fig_dict) html = bundle['text/html'] diff --git a/plotly/io/_html.py b/plotly/io/_html.py index e0da100e891..7618333601d 100644 --- a/plotly/io/_html.py +++ b/plotly/io/_html.py @@ -22,11 +22,13 @@ if (window.MathJax) {MathJax.Hub.Config({SVG: {font: "STIX-Web"}});}\ """ + def to_div(fig, config=None, auto_play=True, include_plotlyjs=True, include_mathjax=False, + post_script=None, validate=True): """ Convert a figure to an HTML div representation @@ -86,7 +88,13 @@ def to_div(fig, If a string that ends in '.js', a script tag is included that references the specified path. This approach can be used to point the resulting HTML div string to an alternative CDN. - validate: bool (default True) + post_script: str or None (default None) + JavaScript snippet to be included in the resulting div just after + plot creation. The string may include '{plot_id}' placeholders that + will then be replaced by the `id` of the div element that the + plotly.js figure is associated with. One application for this script + is to install custom plotly.js event handlers. + validate: bool (default True) True if the figure should be validated before being converted to JSON, False otherwise. Returns @@ -98,7 +106,7 @@ def to_div(fig, fig_dict = validate_coerce_fig_to_dict(fig, validate) # ## Generate div id ## - plotdivid = uuid.uuid4() + plotdivid = str(uuid.uuid4()) # ## Serialize figure ## jdata = json.dumps( @@ -127,14 +135,21 @@ def to_div(fig, # ## Build script body ## # This is the part that actually calls Plotly.js + if post_script: + then_post_script = """.then(function(){{ + {post_script} + }})""".format( + post_script=post_script.replace('{plot_id}', plotdivid)) + else: + then_post_script = '' + if jframes: if auto_play: - animate = """.then(function(){{ + then_animate = """.then(function(){{ Plotly.animate('{id}'); }}""".format(id=plotdivid) - else: - animate = '' + then_animate = '' script = ''' if (document.getElementById("{id}")) {{ @@ -143,7 +158,8 @@ def to_div(fig, {data}, {layout}, {config} - ).then(function () {add_frames}){animate} + ).then(function () {add_frames})\ +{then_animate}{then_post_script} }} '''.format( id=plotdivid, @@ -153,18 +169,21 @@ def to_div(fig, add_frames="{" + "return Plotly.addFrames('{id}',{frames}".format( id=plotdivid, frames=jframes ) + ");}", - animate=animate + then_animate=then_animate, + then_post_script=then_post_script ) else: script = """ if (document.getElementById("{id}")) {{ - Plotly.newPlot("{id}", {data}, {layout}, {config}); + Plotly.newPlot("{id}", {data}, {layout}, {config})\ +{then_post_script} }} """.format( id=plotdivid, data=jdata, layout=jlayout, - config=jconfig) + config=jconfig, + then_post_script=then_post_script) # ## Handle loading/initializing plotly.js ## include_plotlyjs_orig = include_plotlyjs @@ -247,8 +266,8 @@ def to_div(fig, """.format( @@ -268,6 +287,7 @@ def to_html(fig, auto_play=True, include_plotlyjs=True, include_mathjax=False, + post_script=None, validate=True): div = to_div(fig, @@ -275,6 +295,7 @@ def to_html(fig, auto_play=auto_play, include_plotlyjs=include_plotlyjs, include_mathjax=include_mathjax, + post_script=post_script, validate=validate) return """\ @@ -291,6 +312,7 @@ def write_html(fig, auto_play=True, include_plotlyjs=True, include_mathjax=False, + post_script=None, validate=True, auto_open=False): @@ -301,6 +323,7 @@ def write_html(fig, auto_play=auto_play, include_plotlyjs=include_plotlyjs, include_mathjax=include_mathjax, + post_script=post_script, validate=validate) # Check if file is a string diff --git a/plotly/offline/offline.py b/plotly/offline/offline.py index 19c33c6a3a3..56059046745 100644 --- a/plotly/offline/offline.py +++ b/plotly/offline/offline.py @@ -220,18 +220,38 @@ def get_image_download_script(caller): raise ValueError('caller should only be one of `iplot` or `plot`') return( - ('') - ) + 'downloadimage(\'{format}\', {height}, {width}, ' + '\'{filename}\');' + + check_end)) + + +def build_save_image_post_script( + image, image_filename, image_height, image_width, caller): + if image: + if image not in __IMAGE_FORMATS: + raise ValueError('The image parameter must be one of the ' + 'following: {}'.format(__IMAGE_FORMATS) + ) + # if the check passes then download script is injected. + # write the download script: + script = get_image_download_script(caller) + + # Replace none's with nulls + post_script = script.format( + format=image, + width=image_width, + height=image_height, + filename=image_filename) + else: + post_script = None + + return post_script def init_notebook_mode(connected=False): @@ -370,8 +390,8 @@ def _plot_html(figure_or_data, config, validate, default_width, def iplot(figure_or_data, show_link=False, link_text='Export to plot.ly', - validate=True, image=None, filename=None, image_width=None, - image_height=None, config=None, auto_play=True): + validate=True, image=None, filename='plot_image', image_width=800, + image_height=600, config=None, auto_play=True): """ Draw plotly graphs inside an IPython or Jupyter notebook without connecting to an external server. @@ -394,8 +414,16 @@ def iplot(figure_or_data, show_link=False, link_text='Export to plot.ly', has become outdated with your version of graph_reference.json or if you need to include extra, unnecessary keys in your figure. + image (default=None |'png' |'jpeg' |'svg' |'webp') -- This parameter sets + the format of the image to be downloaded, if we choose to download an + image. This parameter has a default value of None indicating that no + image should be downloaded. Please note: for higher resolution images + and more export options, consider making requests to our image servers. + Type: `help(py.image)` for more details. filename (default='plot') -- Sets the name of the file your image will be saved to. The extension should not be included. + image_height (default=600) -- Specifies the height of the image in `px`. + image_width (default=800) -- Specifies the width of the image in `px`. config (default=None) -- Plot view options dictionary. Keyword arguments `show_link` and `link_text` set the associated options in this dictionary if it doesn't contain them already. @@ -431,14 +459,22 @@ def iplot(figure_or_data, show_link=False, link_text='Export to plot.ly', # Get figure figure = tools.return_figure_from_figure_or_data(figure_or_data, validate) + # Handle image request + post_script = build_save_image_post_script( + image, filename, image_height, image_width, 'iplot') + # Show figure - pio.show(figure, validate=validate, config=config, auto_play=auto_play) + pio.show(figure, + validate=validate, + config=config, + auto_play=auto_play, + post_script=post_script) def plot(figure_or_data, show_link=False, link_text='Export to plot.ly', validate=True, output_type='file', include_plotlyjs=True, filename='temp-plot.html', auto_open=True, image=None, - image_filename=None, image_width=None, image_height=None, + image_filename='plot_image', image_width=800, image_height=600, config=None, include_mathjax=False, auto_play=True): """ Create a plotly graph locally as an HTML document or string. @@ -520,6 +556,16 @@ def plot(figure_or_data, show_link=False, link_text='Export to plot.ly', auto_open (default=True) -- If True, open the saved file in a web browser after saving. This argument only applies if `output_type` is 'file'. + image (default=None |'png' |'jpeg' |'svg' |'webp') -- This parameter sets + the format of the image to be downloaded, if we choose to download an + image. This parameter has a default value of None indicating that no + image should be downloaded. Please note: for higher resolution images + and more export options, consider making requests to our image servers. + Type: `help(py.image)` for more details. + image_filename (default='plot_image') -- Sets the name of the file your + image will be saved to. The extension should not be included. + image_height (default=600) -- Specifies the height of the image in `px`. + image_width (default=800) -- Specifies the width of the image in `px`. config (default=None) -- Plot view options dictionary. Keyword arguments `show_link` and `link_text` set the associated options in this dictionary if it doesn't contain them already. @@ -574,6 +620,10 @@ def plot(figure_or_data, show_link=False, link_text='Export to plot.ly', if width == '100%' or height == '100%': config.setdefault('responsive', True) + # Handle image request + post_script = build_save_image_post_script( + image, image_filename, image_height, image_width, 'plot') + if output_type == 'file': pio.write_html( figure, @@ -582,6 +632,7 @@ def plot(figure_or_data, show_link=False, link_text='Export to plot.ly', auto_play=auto_play, include_plotlyjs=include_plotlyjs, include_mathjax=include_mathjax, + post_script=post_script, validate=validate, auto_open=auto_open) return filename @@ -592,6 +643,7 @@ def plot(figure_or_data, show_link=False, link_text='Export to plot.ly', auto_play=auto_play, include_plotlyjs=include_plotlyjs, include_mathjax=include_mathjax, + post_script=post_script, validate=validate) From c4cb4a7cf049f7515ee5568ea225b5ff224912d0 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 20 Mar 2019 08:48:13 -0400 Subject: [PATCH 24/48] Remove default renderer and make rendering figure on display an option that is false by default. Now the behavior of figure display, init_notebook_mode, iplot, and plot are all backward compatible by default. --- plotly/basedatatypes.py | 17 ++++++++++------- plotly/io/_renderers.py | 22 +++++++++++++++++++++- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/plotly/basedatatypes.py b/plotly/basedatatypes.py index 3d6709e20cc..b4b78881735 100644 --- a/plotly/basedatatypes.py +++ b/plotly/basedatatypes.py @@ -397,13 +397,16 @@ def _repr_mimebundle_(self, include, exclude, **kwargs): repr_mimebundle should accept include, exclude and **kwargs """ import plotly.io as pio - data = pio.renderers._build_mime_bundle(self.to_dict()) - - if include: - data = {k: v for (k, v) in data.items() if k in include} - if exclude: - data = {k: v for (k, v) in data.items() if k not in exclude} - return data + if pio.renderers.render_on_display: + data = pio.renderers._build_mime_bundle(self.to_dict()) + + if include: + data = {k: v for (k, v) in data.items() if k in include} + if exclude: + data = {k: v for (k, v) in data.items() if k not in exclude} + return data + else: + return None def update(self, dict1=None, **kwargs): """ diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index 41f80f2afe8..3ec3c7f06cd 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -27,6 +27,7 @@ def __init__(self): self._renderers = {} self._default_name = None self._default_renderers = [] + self._render_on_display = False # ### Magic methods ### # Make this act as a dict of renderers @@ -129,6 +130,25 @@ def default(self, value): for renderer in self._default_renderers: renderer.activate() + @property + def render_on_display(self): + """ + If True, the default mimetype renderers will be used to render + figures when they are displayed in an IPython context. + + Returns + ------- + bool + """ + return self._render_on_display + + @render_on_display.setter + def render_on_display(self, val): + if val: + self._render_on_display = True + else: + self._render_on_display = False + def _validate_coerce_renderers(self, renderers_string): """ Input a string and validate that it contains the names of one or more @@ -374,7 +394,7 @@ def show(fig, renderer=None, validate=True, **kwargs): # # Note that this doesn't cause any problem for offline JupyterLab users. if not default_renderer: - default_renderer = 'notebook_connected+plotly_mimetype' + default_renderer = '' # Set default renderer renderers.default = default_renderer From a109f866f081cc9adfd0a7dadcf2a9575049ddb0 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 20 Mar 2019 08:51:25 -0400 Subject: [PATCH 25/48] Update renderer tests with plotly.io.renderers.render_on_display --- plotly/tests/test_io/test_renderers.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/plotly/tests/test_io/test_renderers.py b/plotly/tests/test_io/test_renderers.py index d8ff588852b..72ce4bdfac9 100644 --- a/plotly/tests/test_io/test_renderers.py +++ b/plotly/tests/test_io/test_renderers.py @@ -34,6 +34,10 @@ def test_json_renderer_mimetype(fig1): pio.renderers.default = 'json' expected = {'application/json': json.loads(pio.to_json(fig1))} + pio.renderers.render_on_display = False + assert fig1._repr_mimebundle_(None, None) is None + + pio.renderers.render_on_display = True bundle = fig1._repr_mimebundle_(None, None) assert bundle == expected @@ -74,6 +78,10 @@ def test_plotly_mimetype_renderer_mimetype(fig1, renderer): expected[plotly_mimetype]['config'] = { 'plotlyServerURL': 'https://plot.ly'} + pio.renderers.render_on_display = False + assert fig1._repr_mimebundle_(None, None) is None + + pio.renderers.render_on_display = True bundle = fig1._repr_mimebundle_(None, None) assert bundle == expected @@ -109,6 +117,10 @@ def test_png_renderer_mimetype(fig1): expected = {'image/png': image_str} + pio.renderers.render_on_display = False + assert fig1._repr_mimebundle_(None, None) is None + + pio.renderers.render_on_display = True bundle = fig1._repr_mimebundle_(None, None) assert bundle == expected @@ -348,6 +360,10 @@ def test_mimetype_combination(fig1): plotly_mimetype: plotly_mimetype_dict, } + pio.renderers.render_on_display = False + assert fig1._repr_mimebundle_(None, None) is None + + pio.renderers.render_on_display = True bundle = fig1._repr_mimebundle_(None, None) assert bundle == expected From f7f7cb7ed4118142879c27ae018e24bce4535237 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 20 Mar 2019 08:59:13 -0400 Subject: [PATCH 26/48] Add download image plotly.offline.plot test --- .../tests/test_core/test_offline/test_offline.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/plotly/tests/test_core/test_offline/test_offline.py b/plotly/tests/test_core/test_offline/test_offline.py index 123b263fcdd..da1a93e21a1 100644 --- a/plotly/tests/test_core/test_offline/test_offline.py +++ b/plotly/tests/test_core/test_offline/test_offline.py @@ -65,6 +65,8 @@ do_auto_play = 'Plotly.animate' +download_image = 'Plotly.downloadImage' + class PlotlyOfflineBaseTestCase(TestCase): def tearDown(self): # Some offline tests produce an html file. Make sure we clean up :) @@ -415,6 +417,18 @@ def test_auto_play(self): self.assertIn(do_auto_play, html) def test_no_auto_play(self): - html = plotly.offline.plot(fig_frames, output_type='div', auto_play=False) + html = plotly.offline.plot( + fig_frames, output_type='div',auto_play=False) self.assertIn(add_frames, html) self.assertNotIn(do_auto_play, html) + + def test_download_image(self): + # Not download image by default + html = plotly.offline.plot( + fig_frames, output_type='div', auto_play=False) + self.assertNotIn(download_image, html) + + # Request download image + html = plotly.offline.plot( + fig_frames, output_type='div', auto_play=False, image='png') + self.assertIn(download_image, html) \ No newline at end of file From f073a1412a6c1e067c3c378f50b1feee334cc297 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 20 Mar 2019 09:18:22 -0400 Subject: [PATCH 27/48] add set_v3_defaults and set_v4_defaults renderer methods to make it easy to preview v4 behavior and revert to v3 behavior. --- plotly/io/_renderers.py | 82 +++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 32 deletions(-) diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index 3ec3c7f06cd..b36d588b76a 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -149,6 +149,54 @@ def render_on_display(self, val): else: self._render_on_display = False + def set_v4_defaults(self): + """ + Configure the rendering system to match the default behavior of + plotly.py version 4 + """ + default_renderer = None + + # Try to detect environment so that we can enable a useful + # default renderer + if ipython_display: + try: + import google.colab + default_renderer = 'colab' + except ImportError: + pass + + # Check if we're running in a Kaggle notebook + if not default_renderer and os.path.exists('/kaggle/input'): + default_renderer = 'kaggle' + + # Check if we're running in VSCode + if not default_renderer and 'VSCODE_PID' in os.environ: + default_renderer = 'vscode' + + # Fallback to renderer combination that will work automatically + # in the classic notebook, jupyterlab, nteract, vscode, and + # nbconvert HTML export. We use 'notebook_connected' rather than + # 'notebook' to avoid bloating notebook size and slowing down + # plotly.py initial import. This comes at the cost of requiring + # internet connectivity to view, but that is a preferable + # trade-off to adding ~3MB to each saved notebook. + # + # Note that this doesn't cause any problem for offline + # JupyterLab users. + if not default_renderer: + default_renderer = 'notebook_connected+plotly_mimetype' + + renderers.render_on_display = True + self.default = default_renderer + + def set_v3_defaults(self): + """ + Configure the rendering system to match the default behavior of + plotly.py version 3 + """ + self.render_on_display = False + self.default = 'plotly_mimetype' + def _validate_coerce_renderers(self, renderers_string): """ Input a string and validate that it contains the names of one or more @@ -366,35 +414,5 @@ def show(fig, renderer=None, validate=True, **kwargs): # Set default renderer # -------------------- -default_renderer = None - -# Try to detect environment so that we can enable a useful default renderer -if ipython_display: - try: - import google.colab - default_renderer = 'colab' - except ImportError: - pass - - # Check if we're running in a Kaggle notebook - if not default_renderer and os.path.exists('/kaggle/input'): - default_renderer = 'kaggle' - - # Check if we're running in VSCode - if not default_renderer and 'VSCODE_PID' in os.environ: - default_renderer = 'vscode' - - # Fallback to renderer combination that will work automatically in the - # classic notebook, jupyterlab, nteract, vscode, and nbconvert HTML - # export. We use 'notebook_connected' rather than 'notebook' to avoid - # bloating notebook size and slowing down plotly.py initial import. - # This comes at the cost of requiring internet connectivity to view, - # but that is a preferable trade-off to adding ~3MB to each saved - # notebook. - # - # Note that this doesn't cause any problem for offline JupyterLab users. - if not default_renderer: - default_renderer = '' - -# Set default renderer -renderers.default = default_renderer +# This will change to renderers.set_v4_defaults() in version 4.0 +renderers.set_v3_defaults() From 105868353cc24e87322fe4419a4bfc6b2ebcbc64 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 20 Mar 2019 11:34:32 -0400 Subject: [PATCH 28/48] Added future flag system under _plotly_future_ to control default renderer settings and default template. Get all of v4 settings with: ```python from _plotly_future_ import v4 ``` --- _plotly_future_/__init__.py | 8 +++ _plotly_future_/renderer_defaults.py | 5 ++ _plotly_future_/template_defaults.py | 5 ++ _plotly_future_/v4.py | 2 + plotly/__init__.py | 5 ++ plotly/io/_renderers.py | 89 +++++++++++++--------------- plotly/io/_templates.py | 2 - setup.py | 4 +- 8 files changed, 68 insertions(+), 52 deletions(-) create mode 100644 _plotly_future_/__init__.py create mode 100644 _plotly_future_/renderer_defaults.py create mode 100644 _plotly_future_/template_defaults.py create mode 100644 _plotly_future_/v4.py diff --git a/_plotly_future_/__init__.py b/_plotly_future_/__init__.py new file mode 100644 index 00000000000..0b1b851d34b --- /dev/null +++ b/_plotly_future_/__init__.py @@ -0,0 +1,8 @@ +_future_flags = set() + + +def _assert_plotly_not_imported(): + import sys + if 'plotly' in sys.modules: + raise ImportError("""\ +The _plotly_future_ module must be import before the plotly module""") diff --git a/_plotly_future_/renderer_defaults.py b/_plotly_future_/renderer_defaults.py new file mode 100644 index 00000000000..1ee0e54301b --- /dev/null +++ b/_plotly_future_/renderer_defaults.py @@ -0,0 +1,5 @@ +from __future__ import absolute_import +from _plotly_future_ import _future_flags, _assert_plotly_not_imported + +_assert_plotly_not_imported() +_future_flags.add('renderer_defaults') diff --git a/_plotly_future_/template_defaults.py b/_plotly_future_/template_defaults.py new file mode 100644 index 00000000000..82c0f52ff2d --- /dev/null +++ b/_plotly_future_/template_defaults.py @@ -0,0 +1,5 @@ +from __future__ import absolute_import +from _plotly_future_ import _future_flags, _assert_plotly_not_imported + +_assert_plotly_not_imported() +_future_flags.add('template_defaults') diff --git a/_plotly_future_/v4.py b/_plotly_future_/v4.py new file mode 100644 index 00000000000..fdbad2b012c --- /dev/null +++ b/_plotly_future_/v4.py @@ -0,0 +1,2 @@ +from __future__ import absolute_import +from _plotly_future_ import renderer_defaults, template_defaults diff --git a/plotly/__init__.py b/plotly/__init__.py index 0789f76601b..8d70b5749df 100644 --- a/plotly/__init__.py +++ b/plotly/__init__.py @@ -31,7 +31,12 @@ from plotly import (plotly, dashboard_objs, graph_objs, grid_objs, tools, utils, session, offline, colors, io) from plotly.version import __version__ +from _plotly_future_ import _future_flags from ._version import get_versions __version__ = get_versions()['version'] del get_versions + +# Set default template here to make sure import process is complet +if 'template_defaults' in _future_flags: + io.templates._default = 'plotly' diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index b36d588b76a..1db93fc1e11 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -7,6 +7,7 @@ import os from plotly import optional_imports +from _plotly_future_ import _future_flags from plotly.io._base_renderers import ( MimetypeRenderer, SideEffectRenderer, PlotlyRenderer, NotebookRenderer, @@ -149,54 +150,6 @@ def render_on_display(self, val): else: self._render_on_display = False - def set_v4_defaults(self): - """ - Configure the rendering system to match the default behavior of - plotly.py version 4 - """ - default_renderer = None - - # Try to detect environment so that we can enable a useful - # default renderer - if ipython_display: - try: - import google.colab - default_renderer = 'colab' - except ImportError: - pass - - # Check if we're running in a Kaggle notebook - if not default_renderer and os.path.exists('/kaggle/input'): - default_renderer = 'kaggle' - - # Check if we're running in VSCode - if not default_renderer and 'VSCODE_PID' in os.environ: - default_renderer = 'vscode' - - # Fallback to renderer combination that will work automatically - # in the classic notebook, jupyterlab, nteract, vscode, and - # nbconvert HTML export. We use 'notebook_connected' rather than - # 'notebook' to avoid bloating notebook size and slowing down - # plotly.py initial import. This comes at the cost of requiring - # internet connectivity to view, but that is a preferable - # trade-off to adding ~3MB to each saved notebook. - # - # Note that this doesn't cause any problem for offline - # JupyterLab users. - if not default_renderer: - default_renderer = 'notebook_connected+plotly_mimetype' - - renderers.render_on_display = True - self.default = default_renderer - - def set_v3_defaults(self): - """ - Configure the rendering system to match the default behavior of - plotly.py version 3 - """ - self.render_on_display = False - self.default = 'plotly_mimetype' - def _validate_coerce_renderers(self, renderers_string): """ Input a string and validate that it contains the names of one or more @@ -415,4 +368,42 @@ def show(fig, renderer=None, validate=True, **kwargs): # Set default renderer # -------------------- # This will change to renderers.set_v4_defaults() in version 4.0 -renderers.set_v3_defaults() +if 'renderer_defaults' in _future_flags: + default_renderer = None + + # Try to detect environment so that we can enable a useful + # default renderer + if ipython_display: + try: + import google.colab + + default_renderer = 'colab' + except ImportError: + pass + + # Check if we're running in a Kaggle notebook + if not default_renderer and os.path.exists('/kaggle/input'): + default_renderer = 'kaggle' + + # Check if we're running in VSCode + if not default_renderer and 'VSCODE_PID' in os.environ: + default_renderer = 'vscode' + + # Fallback to renderer combination that will work automatically + # in the classic notebook, jupyterlab, nteract, vscode, and + # nbconvert HTML export. We use 'notebook_connected' rather than + # 'notebook' to avoid bloating notebook size and slowing down + # plotly.py initial import. This comes at the cost of requiring + # internet connectivity to view, but that is a preferable + # trade-off to adding ~3MB to each saved notebook. + # + # Note that this doesn't cause any problem for offline + # JupyterLab users. + if not default_renderer: + default_renderer = 'notebook_connected+plotly_mimetype' + + renderers.render_on_display = True + renderers.default = default_renderer +else: + renderers.render_on_display = False + renderers.default = 'plotly_mimetype' diff --git a/plotly/io/_templates.py b/plotly/io/_templates.py index e6c25ce49e1..3fd1377f586 100644 --- a/plotly/io/_templates.py +++ b/plotly/io/_templates.py @@ -1,5 +1,4 @@ from __future__ import absolute_import - from plotly.basedatatypes import BaseFigure from plotly.graph_objs import Figure from plotly.validators.layout import TemplateValidator @@ -238,7 +237,6 @@ def _merge_2_templates(self, template1, template2): templates = TemplatesConfig() del TemplatesConfig - # Template utilities # ------------------ def walk_push_to_template(fig_obj, template_obj, skip): diff --git a/setup.py b/setup.py index 747d3f5e186..3cfe1cbcb7b 100644 --- a/setup.py +++ b/setup.py @@ -425,7 +425,9 @@ def run(self): 'plotly/matplotlylib', 'plotly/matplotlylib/mplexporter', 'plotly/matplotlylib/mplexporter/renderers', - '_plotly_utils'] + graph_objs_packages + validator_packages, + '_plotly_utils', + '_plotly_future_', + ] + graph_objs_packages + validator_packages, package_data={'plotly': ['package_data/*', 'package_data/templates/*'], 'plotlywidget': ['static/extension.js', 'static/index.js']}, From df3515ee0848ca734e760523425ad47c413c362c Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Wed, 20 Mar 2019 18:30:10 -0400 Subject: [PATCH 29/48] Roll to_div function into to_html with full_html argument Docstrings for to_html and write_html --- plotly/io/__init__.py | 2 +- plotly/io/_base_renderers.py | 39 ++---- plotly/io/_html.py | 258 ++++++++++++++++++++++++----------- plotly/offline/offline.py | 4 +- 4 files changed, 196 insertions(+), 107 deletions(-) diff --git a/plotly/io/__init__.py b/plotly/io/__init__.py index 119436755d7..c0478c70ac0 100644 --- a/plotly/io/__init__.py +++ b/plotly/io/__init__.py @@ -9,4 +9,4 @@ from . import base_renderers -from ._html import to_div, to_html, write_html +from ._html import to_html, write_html diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index 447b52bd42b..edcd0247c45 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -1,7 +1,6 @@ import base64 import json import webbrowser -import uuid import inspect import os @@ -220,7 +219,7 @@ class HtmlRenderer(MimetypeRenderer): """ def __init__(self, connected=False, - fullhtml=False, + full_html=False, requirejs=True, global_init=False, config=None, @@ -232,7 +231,7 @@ def __init__(self, self.connected = connected self.global_init = global_init self.requirejs = requirejs - self.fullhtml = fullhtml + self.full_html = full_html self.post_script = post_script def activate(self): @@ -292,7 +291,7 @@ def activate(self): def to_mimebundle(self, fig_dict): - from plotly.io import to_div, to_html + from plotly.io import to_html if self.requirejs: include_plotlyjs = 'require' @@ -304,22 +303,14 @@ def to_mimebundle(self, fig_dict): include_plotlyjs = True include_mathjax = 'cdn' - if self.fullhtml: - html = to_html( - fig_dict, - config=self.config, - auto_play=self.auto_play, - include_plotlyjs=include_plotlyjs, - include_mathjax=include_mathjax, - post_script=self.post_script) - else: - html = to_div( - fig_dict, - config=self.config, - auto_play=self.auto_play, - include_plotlyjs=include_plotlyjs, - include_mathjax=include_mathjax, - post_script=self.post_script) + html = to_html( + fig_dict, + config=self.config, + auto_play=self.auto_play, + include_plotlyjs=include_plotlyjs, + include_mathjax=include_mathjax, + post_script=self.post_script, + full_html=self.full_html) return {'text/html': html} @@ -343,7 +334,7 @@ def __init__(self, post_script=None): super(NotebookRenderer, self).__init__( connected=connected, - fullhtml=False, + full_html=False, requirejs=True, global_init=True, config=config, @@ -365,7 +356,7 @@ class KaggleRenderer(HtmlRenderer): def __init__(self, config=None, auto_play=False, post_script=None): super(KaggleRenderer, self).__init__( connected=True, - fullhtml=False, + full_html=False, requirejs=True, global_init=True, config=config, @@ -384,7 +375,7 @@ class ColabRenderer(HtmlRenderer): def __init__(self, config=None, auto_play=False, post_script=None): super(ColabRenderer, self).__init__( connected=True, - fullhtml=True, + full_html=True, requirejs=False, global_init=False, config=config, @@ -565,7 +556,7 @@ def __init__(self, def render(self, fig_dict): renderer = HtmlRenderer( connected=False, - fullhtml=True, + full_html=True, requirejs=False, global_init=False, config=self.config, diff --git a/plotly/io/_html.py b/plotly/io/_html.py index 7618333601d..5bc4cff6947 100644 --- a/plotly/io/_html.py +++ b/plotly/io/_html.py @@ -23,15 +23,16 @@ """ -def to_div(fig, - config=None, - auto_play=True, - include_plotlyjs=True, - include_mathjax=False, - post_script=None, - validate=True): +def to_html(fig, + config=None, + auto_play=True, + include_plotlyjs=True, + include_mathjax=False, + post_script=None, + full_html=True, + validate=True): """ - Convert a figure to an HTML div representation + Convert a figure to an HTML string representation. Parameters ---------- @@ -64,6 +65,8 @@ def to_div(fig, If 'require', Plotly.js is loaded using require.js. This option assumes that require.js is globally available and that it has been globally configured to know how to find Plotly.js as 'plotly'. + This option is not advised when full_html=True as it will result + in a non-functional html file. If a string that ends in '.js', a script tag is included that references the specified path. This approach can be used to point @@ -71,7 +74,8 @@ def to_div(fig, If False, no script tag referencing plotly.js is included. This is useful when the resulting div string will be placed inside an HTML - document that already loads plotly.js. + document that already loads plotly.js. This option is not advised + when full_html=True as it will result in a non-functional html file. include_mathjax: bool or string (default False) Specifies how the MathJax.js library is included in the output html div string. MathJax is required in order to display labels @@ -94,6 +98,10 @@ def to_div(fig, will then be replaced by the `id` of the div element that the plotly.js figure is associated with. One application for this script is to install custom plotly.js event handlers. + full_html: bool (default True) + If True, produce a string containing a complete HTML document + starting with an tag. If False, produce a string containing + a single
element. validate: bool (default True) True if the figure should be validated before being converted to JSON, False otherwise. @@ -102,6 +110,7 @@ def to_div(fig, str Representation of figure as an HTML div string """ + # ## Validate figure ## fig_dict = validate_coerce_fig_to_dict(fig, validate) @@ -137,8 +146,8 @@ def to_div(fig, # This is the part that actually calls Plotly.js if post_script: then_post_script = """.then(function(){{ - {post_script} - }})""".format( + {post_script} + }})""".format( post_script=post_script.replace('{plot_id}', plotdivid)) else: then_post_script = '' @@ -146,22 +155,22 @@ def to_div(fig, if jframes: if auto_play: then_animate = """.then(function(){{ - Plotly.animate('{id}'); - }}""".format(id=plotdivid) + Plotly.animate('{id}'); + }}""".format(id=plotdivid) else: then_animate = '' script = ''' - if (document.getElementById("{id}")) {{ - Plotly.plot( - '{id}', - {data}, - {layout}, - {config} - ).then(function () {add_frames})\ -{then_animate}{then_post_script} - }} - '''.format( + if (document.getElementById("{id}")) {{ + Plotly.plot( + '{id}', + {data}, + {layout}, + {config} + ).then(function () {add_frames})\ + {then_animate}{then_post_script} + }} + '''.format( id=plotdivid, data=jdata, layout=jlayout, @@ -174,11 +183,11 @@ def to_div(fig, ) else: script = """ - if (document.getElementById("{id}")) {{ - Plotly.newPlot("{id}", {data}, {layout}, {config})\ -{then_post_script} - }} - """.format( + if (document.getElementById("{id}")) {{ + Plotly.newPlot("{id}", {data}, {layout}, {config})\ + {then_post_script} + }} + """.format( id=plotdivid, data=jdata, layout=jlayout, @@ -205,30 +214,30 @@ def to_div(fig, elif include_plotlyjs == 'cdn': load_plotlyjs = """\ - {win_config} - \ -""".format(win_config=_window_plotly_config) + {win_config} + \ + """.format(win_config=_window_plotly_config) elif include_plotlyjs == 'directory': load_plotlyjs = """\ - {win_config} - \ -""".format(win_config=_window_plotly_config) + {win_config} + \ + """.format(win_config=_window_plotly_config) elif (isinstance(include_plotlyjs, six.string_types) and include_plotlyjs.endswith('.js')): load_plotlyjs = """\ - {win_config} - \ -""".format(win_config=_window_plotly_config, - url=include_plotlyjs_orig) + {win_config} + \ + """.format(win_config=_window_plotly_config, + url=include_plotlyjs_orig) elif include_plotlyjs: load_plotlyjs = """\ - {win_config} - \ -""".format(win_config=_window_plotly_config, - plotlyjs=get_plotlyjs()) + {win_config} + \ + """.format(win_config=_window_plotly_config, + plotlyjs=get_plotlyjs()) # ## Handle loading/initializing MathJax ## include_mathjax_orig = include_mathjax @@ -236,11 +245,11 @@ def to_div(fig, include_mathjax = include_mathjax.lower() mathjax_template = """\ -""" + """ if include_mathjax == 'cdn': mathjax_script = mathjax_template.format( - url=('https://cdnjs.cloudflare.com' + url=('https://cdnjs.cloudflare.com' '/ajax/libs/mathjax/2.7.5/MathJax.js')) + _mathjax_config elif (isinstance(include_mathjax, six.string_types) and @@ -252,25 +261,25 @@ def to_div(fig, mathjax_script = '' else: raise ValueError("""\ -Invalid value of type {typ} received as the include_mathjax argument - Received value: {val} + Invalid value of type {typ} received as the include_mathjax argument + Received value: {val} -include_mathjax may be specified as False, 'cdn', or a string ending with '.js' -""".format(typ=type(include_mathjax), val=repr(include_mathjax))) + include_mathjax may be specified as False, 'cdn', or a string ending with '.js' + """.format(typ=type(include_mathjax), val=repr(include_mathjax))) plotly_html_div = """\
- {mathjax_script} - {load_plotlyjs} -
- -
""".format( + {mathjax_script} + {load_plotlyjs} +
+ +
""".format( mathjax_script=mathjax_script, load_plotlyjs=load_plotlyjs, id=plotdivid, @@ -279,31 +288,16 @@ def to_div(fig, script=script, require_end=require_end) - return plotly_html_div - - -def to_html(fig, - config=None, - auto_play=True, - include_plotlyjs=True, - include_mathjax=False, - post_script=None, - validate=True): - - div = to_div(fig, - config=config, - auto_play=auto_play, - include_plotlyjs=include_plotlyjs, - include_mathjax=include_mathjax, - post_script=post_script, - validate=validate) - return """\ + if full_html: + return """\ {div} -""".format(div=div) +""".format(div=plotly_html_div) + else: + return plotly_html_div def write_html(fig, @@ -313,8 +307,109 @@ def write_html(fig, include_plotlyjs=True, include_mathjax=False, post_script=None, + full_html=True, validate=True, auto_open=False): + """ + Write a figure to an HTML file representation + + Parameters + ---------- + fig: + Figure object or dict representing a figure + file: str or writeable + A string representing a local file path or a writeable object + (e.g. an open file descriptor) + config: dict or None (default None) + Plotly.js figure config options + auto_play: bool (default=True) + Whether to automatically start the animation sequence on page load + if the figure contains frames. Has no effect if the figure does not + contain frames. + include_plotlyjs: bool or string (default True) + Specifies how the plotly.js library is included/loaded in the output + 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 `file` is a string to a local file path + and `full_html` is True then + + 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 `file` is a string to a local file + path and `full_html` is True, 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 'require', Plotly.js is loaded using require.js. This option + assumes that require.js is globally available and that it has been + globally configured to know how to find Plotly.js as 'plotly'. + This option is not advised when full_html=True as it will result + in a non-functional html file. + + If a string that ends in '.js', a script tag is included that + references the specified path. This approach can be used to point + the resulting HTML file to an alternative CDN or local bundle. + + If False, no script tag referencing plotly.js is included. This is + useful when the resulting div string will be placed inside an HTML + document that already loads plotly.js. This option is not advised + when full_html=True as it will result in a non-functional html file. + + include_mathjax: bool or string (default False) + Specifies how the MathJax.js library is included in the output html + div string. MathJax is required in order to display labels + with LaTeX typesetting. + + If False, no script tag referencing MathJax.js will be included in the + output. + + If 'cdn', a script tag that references a MathJax CDN location will be + included in the output. HTML div strings generated with this option + will be able to display LaTeX typesetting as long as internet access + is available. + + If a string that ends in '.js', a script tag is included that + references the specified path. This approach can be used to point the + resulting HTML div string to an alternative CDN. + post_script: str or None (default None) + JavaScript snippet to be included in the resulting div just after + plot creation. The string may include '{plot_id}' placeholders that + will then be replaced by the `id` of the div element that the + plotly.js figure is associated with. One application for this script + is to install custom plotly.js event handlers. + full_html: bool (default True) + If True, produce a string containing a complete HTML document + starting with an tag. If False, produce a string containing + a single
element. + validate: bool (default True) + True if the figure should be validated before being converted to + JSON, False otherwise. + auto_open: bool (default True + If True, open the saved file in a web browser after saving. + This argument only applies if `full_html` is True. + Returns + ------- + str + Representation of figure as an HTML div string + """ # Build HTML string html_str = to_html( @@ -324,6 +419,7 @@ def write_html(fig, include_plotlyjs=include_plotlyjs, include_mathjax=include_mathjax, post_script=post_script, + full_html=full_html, validate=validate) # Check if file is a string @@ -337,7 +433,7 @@ def write_html(fig, file.write(html_str) # Check if we should copy plotly.min.js to output directory - if file_is_str and include_plotlyjs == 'directory': + if file_is_str and full_html and include_plotlyjs == 'directory': bundle_path = os.path.join( os.path.dirname(file), 'plotly.min.js') @@ -346,6 +442,6 @@ def write_html(fig, f.write(get_plotlyjs()) # Handle auto_open - if file_is_str and auto_open: + if file_is_str and full_html and auto_open: url = 'file://' + os.path.abspath(file) webbrowser.open(url) diff --git a/plotly/offline/offline.py b/plotly/offline/offline.py index 56059046745..408a50670d7 100644 --- a/plotly/offline/offline.py +++ b/plotly/offline/offline.py @@ -633,17 +633,19 @@ def plot(figure_or_data, show_link=False, link_text='Export to plot.ly', include_plotlyjs=include_plotlyjs, include_mathjax=include_mathjax, post_script=post_script, + full_html=True, validate=validate, auto_open=auto_open) return filename else: - return pio.to_div( + return pio.to_html( figure, config=config, auto_play=auto_play, include_plotlyjs=include_plotlyjs, include_mathjax=include_mathjax, post_script=post_script, + full_html=False, validate=validate) From 86eb69a48c8711bcdc3941a0d4315b2d5cbec532 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Thu, 21 Mar 2019 18:03:10 -0400 Subject: [PATCH 30/48] Rename SideEffectRenderer -> ExternalRenderer --- plotly/io/_base_renderers.py | 8 ++++---- plotly/io/_renderers.py | 22 +++++++++++----------- plotly/io/base_renderers.py | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index edcd0247c45..823d1142124 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -468,13 +468,13 @@ def to_mimebundle(self, fig_dict): return {'text/html': iframe_html} -class SideEffectRenderer(RendererRepr): +class ExternalRenderer(RendererRepr): """ - Base class for side-effect renderers. SideEffectRenderer subclasses + Base class for external renderers. ExternalRenderer subclasses do not display figures inline in a notebook environment, but render figures by some external means (e.g. a separate browser tab). - Unlike MimetypeRenderer subclasses, SideEffectRenderer subclasses are not + Unlike MimetypeRenderer subclasses, ExternalRenderer subclasses are not invoked when a figure is asked to display itself in the notebook. Instead, they are invoked when the plotly.io.show function is called on a figure. @@ -526,7 +526,7 @@ def log_message(self, format, *args): server.handle_request() -class BrowserRenderer(SideEffectRenderer): +class BrowserRenderer(ExternalRenderer): """ Renderer to display interactive figures in an external web browser. This renderer will open a new browser window or tab when the diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index 1db93fc1e11..6a759ed012a 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -10,7 +10,7 @@ from _plotly_future_ import _future_flags from plotly.io._base_renderers import ( - MimetypeRenderer, SideEffectRenderer, PlotlyRenderer, NotebookRenderer, + MimetypeRenderer, ExternalRenderer, PlotlyRenderer, NotebookRenderer, KaggleRenderer, ColabRenderer, JsonRenderer, PngRenderer, JpegRenderer, SvgRenderer, PdfRenderer, BrowserRenderer, IFrameRenderer) from plotly.io._utils import validate_coerce_fig_to_dict @@ -46,9 +46,9 @@ def __getitem__(self, item): return renderer def __setitem__(self, key, value): - if not isinstance(value, (MimetypeRenderer, SideEffectRenderer)): + if not isinstance(value, (MimetypeRenderer, ExternalRenderer)): raise ValueError("""\ -Renderer must be a subclass of MimetypeRenderer or SideEffectRenderer. +Renderer must be a subclass of MimetypeRenderer or ExternalRenderer. Received value with type: {typ}""".format(typ=type(value))) self._renderers[key] = value @@ -241,15 +241,15 @@ def _build_mime_bundle(self, fig_dict, renderers_string=None, **kwargs): return bundle - def _perform_side_effect_rendering( + def _perform_external_rendering( self, fig_dict, renderers_string=None, **kwargs): """ - Perform side-effect rendering for each SideEffectRenderer specified + Perform external rendering for each ExternalRenderer specified in either the default renderer string, or in the supplied renderers_string argument. Note that this method skips any renderers that are not subclasses - of SideEffectRenderer. + of ExternalRenderer. Parameters ---------- @@ -267,13 +267,13 @@ def _perform_side_effect_rendering( renderer_names = self._validate_coerce_renderers(renderers_string) renderers_list = [self[name] for name in renderer_names] for renderer in renderers_list: - if isinstance(renderer, SideEffectRenderer): + if isinstance(renderer, ExternalRenderer): renderer.activate() else: renderers_list = self._default_renderers for renderer in renderers_list: - if isinstance(renderer, SideEffectRenderer): + if isinstance(renderer, ExternalRenderer): renderer = copy(renderer) for k, v in kwargs.items(): if hasattr(renderer, k): @@ -324,8 +324,8 @@ def show(fig, renderer=None, validate=True, **kwargs): ipython_display.display(bundle, raw=True) - # Side effect renderers - renderers._perform_side_effect_rendering( + # external renderers + renderers._perform_external_rendering( fig_dict, renderers_string=renderer, **kwargs) @@ -359,7 +359,7 @@ def show(fig, renderer=None, validate=True, **kwargs): renderers['svg'] = SvgRenderer(**img_kwargs) renderers['pdf'] = PdfRenderer(**img_kwargs) -# Side effects +# External renderers['browser'] = BrowserRenderer(config=config) renderers['firefox'] = BrowserRenderer(config=config, using='firefox') renderers['chrome'] = BrowserRenderer(config=config, using='chrome') diff --git a/plotly/io/base_renderers.py b/plotly/io/base_renderers.py index e3ceb0c8906..6042296a644 100644 --- a/plotly/io/base_renderers.py +++ b/plotly/io/base_renderers.py @@ -1,5 +1,5 @@ from ._base_renderers import ( MimetypeRenderer, PlotlyRenderer, JsonRenderer, ImageRenderer, PngRenderer, SvgRenderer, PdfRenderer, JpegRenderer, HtmlRenderer, - ColabRenderer, KaggleRenderer, NotebookRenderer, SideEffectRenderer, + ColabRenderer, KaggleRenderer, NotebookRenderer, ExternalRenderer, BrowserRenderer) From c209a1875daa6f782b17db374afc2063a8d0289d Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 23 Mar 2019 06:17:27 -0400 Subject: [PATCH 31/48] Fix optional test suite --- plotly/io/_html.py | 8 ++++-- .../test_offline/test_offline.py | 28 +++++++++++-------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/plotly/io/_html.py b/plotly/io/_html.py index 5bc4cff6947..b5135752384 100644 --- a/plotly/io/_html.py +++ b/plotly/io/_html.py @@ -119,9 +119,13 @@ def to_html(fig, # ## Serialize figure ## jdata = json.dumps( - fig_dict.get('data', []), cls=utils.PlotlyJSONEncoder) + fig_dict.get('data', []), + cls=utils.PlotlyJSONEncoder, + sort_keys=True) jlayout = json.dumps( - fig_dict.get('layout', {}), cls=utils.PlotlyJSONEncoder) + fig_dict.get('layout', {}), + cls=utils.PlotlyJSONEncoder, + sort_keys=True) if fig_dict.get('frames', None): jframes = json.dumps( diff --git a/plotly/tests/test_optional/test_offline/test_offline.py b/plotly/tests/test_optional/test_offline/test_offline.py index 7f01e4fe3c1..6b2b2e05dc5 100644 --- a/plotly/tests/test_optional/test_offline/test_offline.py +++ b/plotly/tests/test_optional/test_offline/test_offline.py @@ -3,7 +3,7 @@ """ from __future__ import absolute_import - +import re from nose.tools import raises from nose.plugins.attrib import attr from requests.compat import json as _json @@ -32,10 +32,6 @@ def setUp(self): def test_iplot_works_without_init_notebook_mode(self): plotly.offline.iplot([{}]) - @raises(plotly.exceptions.PlotlyError) - def test_iplot_doesnt_work_before_you_call_init_notebook_mode_when_requesting_download(self): - plotly.offline.iplot([{}], image='png') - def test_iplot_works_after_you_call_init_notebook_mode(self): plotly.offline.init_notebook_mode() plotly.offline.iplot([{}]) @@ -74,17 +70,25 @@ def test_default_mpl_plot_generates_expected_html(self): y = [100, 200, 300] plt.plot(x, y) - figure = plotly.tools.mpl_to_plotly(fig) + figure = plotly.tools.mpl_to_plotly(fig).to_dict() data = figure['data'] + layout = figure['layout'] - data_json = _json.dumps(data, cls=plotly.utils.PlotlyJSONEncoder) - layout_json = _json.dumps(layout, cls=plotly.utils.PlotlyJSONEncoder) + data_json = _json.dumps( + data, cls=plotly.utils.PlotlyJSONEncoder, sort_keys=True) + layout_json = _json.dumps( + layout, cls=plotly.utils.PlotlyJSONEncoder, sort_keys=True) html = self._read_html(plotly.offline.plot_mpl(fig)) + # blank out uid before comparisons + data_json = re.sub('"uid": "[^"]+"', '"uid": ""', data_json) + html = re.sub('"uid": "[^"]+"', '"uid": ""', html) + # just make sure a few of the parts are in here # like PlotlyOfflineTestCase(TestCase) in test_core - self.assertTrue(data_json.split('"uid":')[0] in html) # data is in there - self.assertTrue(layout_json in html) # layout is in there too - self.assertTrue(PLOTLYJS in html) # and the source code + self.assertTrue(data_json in html) # data is in there + self.assertTrue(layout_json in html) # layout is in there too + self.assertTrue(PLOTLYJS in html) # and the source code # and it's an doc - self.assertTrue(html.startswith('') and html.endswith('')) + self.assertTrue(html.startswith('') + and html.endswith('')) From 19de7b94bc26abbe563dbed57e190d01b1508fea Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 23 Mar 2019 06:45:44 -0400 Subject: [PATCH 32/48] Use correct config property to grab platform url from saved config --- plotly/io/_html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plotly/io/_html.py b/plotly/io/_html.py index b5135752384..f13eae3be4d 100644 --- a/plotly/io/_html.py +++ b/plotly/io/_html.py @@ -144,7 +144,7 @@ def to_html(fig, jconfig = json.dumps(config) # ## Get platform URL ## - plotly_platform_url = config.get('plotly_domain', 'https://plot.ly') + plotly_platform_url = config.get('plotlyServerURL', 'https://plot.ly') # ## Build script body ## # This is the part that actually calls Plotly.js From ecf48670cd2ee05e8607e68d11e3020d3c0c03c9 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 23 Mar 2019 06:46:22 -0400 Subject: [PATCH 33/48] Remove legacy unused _plot_html function --- plotly/offline/offline.py | 98 --------------------------------------- 1 file changed, 98 deletions(-) diff --git a/plotly/offline/offline.py b/plotly/offline/offline.py index 408a50670d7..28a38b3c0c2 100644 --- a/plotly/offline/offline.py +++ b/plotly/offline/offline.py @@ -291,104 +291,6 @@ def init_notebook_mode(connected=False): pio.renderers.default = 'notebook+plotly_mimetype' -def _plot_html(figure_or_data, config, validate, default_width, - default_height, global_requirejs, auto_play): - - figure = tools.return_figure_from_figure_or_data(figure_or_data, validate) - - width = figure.get('layout', {}).get('width', default_width) - height = figure.get('layout', {}).get('height', default_height) - - try: - float(width) - except (ValueError, TypeError): - pass - else: - width = str(width) + 'px' - - try: - float(height) - except (ValueError, TypeError): - pass - else: - height = str(height) + 'px' - - plotdivid = uuid.uuid4() - jdata = _json.dumps(figure.get('data', []), cls=utils.PlotlyJSONEncoder) - jlayout = _json.dumps(figure.get('layout', {}), - cls=utils.PlotlyJSONEncoder) - - if figure.get('frames', None): - jframes = _json.dumps(figure.get('frames', []), - cls=utils.PlotlyJSONEncoder) - else: - jframes = None - - jconfig = _json.dumps(_get_jconfig(config)) - plotly_platform_url = plotly.plotly.get_config().get('plotly_domain', - 'https://plot.ly') - - if jframes: - if auto_play: - animate = (".then(function(){" + - "Plotly.animate('{id}');".format(id=plotdivid) + - "})") - else: - animate = '' - - script = ''' - if (document.getElementById("{id}")) {{ - Plotly.plot( - '{id}', - {data}, - {layout}, - {config} - ).then(function () {add_frames}){animate} - }} - '''.format( - id=plotdivid, - data=jdata, - layout=jlayout, - config=jconfig, - add_frames="{" + "return Plotly.addFrames('{id}',{frames}".format( - id=plotdivid, frames=jframes - ) + ");}", - animate=animate - ) - else: - script = """ -if (document.getElementById("{id}")) {{ - Plotly.newPlot("{id}", {data}, {layout}, {config}); -}} -""".format( - id=plotdivid, - data=jdata, - layout=jlayout, - config=jconfig) - - optional_line1 = ('require(["plotly"], function(Plotly) {{ ' - if global_requirejs else '') - optional_line2 = ('}});' if global_requirejs else '') - - plotly_html_div = ( - '' - '
' - '
' - '' - '').format( - id=plotdivid, script=script, - height=height, width=width) - - return plotly_html_div, plotdivid, width, height - - def iplot(figure_or_data, show_link=False, link_text='Export to plot.ly', validate=True, image=None, filename='plot_image', image_width=800, image_height=600, config=None, auto_play=True): From b87ebed6ba0b34a084b36182d72c719364040762 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 23 Mar 2019 06:54:49 -0400 Subject: [PATCH 34/48] Move static image renderer tests to orca test suite so that orca is installed on circleci --- plotly/tests/test_io/test_renderers.py | 79 +------------- .../tests/test_orca/test_image_renderers.py | 100 ++++++++++++++++++ 2 files changed, 101 insertions(+), 78 deletions(-) create mode 100644 plotly/tests/test_orca/test_image_renderers.py diff --git a/plotly/tests/test_io/test_renderers.py b/plotly/tests/test_io/test_renderers.py index 72ce4bdfac9..2ba24c68c53 100644 --- a/plotly/tests/test_io/test_renderers.py +++ b/plotly/tests/test_io/test_renderers.py @@ -103,84 +103,7 @@ def test_plotly_mimetype_renderer_show(fig1, renderer): # Static Image # ------------ -def test_png_renderer_mimetype(fig1): - pio.renderers.default = 'png' - - # Configure renderer so that we can use the same parameters - # to build expected image below - pio.renderers['png'].width = 400 - pio.renderers['png'].height = 500 - pio.renderers['png'].scale = 1 - - image_bytes = pio.to_image(fig1, width=400, height=500, scale=1) - image_str = base64.b64encode(image_bytes).decode('utf8') - - expected = {'image/png': image_str} - - pio.renderers.render_on_display = False - assert fig1._repr_mimebundle_(None, None) is None - - pio.renderers.render_on_display = True - bundle = fig1._repr_mimebundle_(None, None) - assert bundle == expected - - -def test_svg_renderer_show(fig1): - pio.renderers.default = 'svg' - pio.renderers['svg'].width = 400 - pio.renderers['svg'].height = 500 - pio.renderers['svg'].scale = 1 - - with mock.patch('IPython.display.display') as mock_display: - pio.show(fig1) - - # Check call args. - # SVGs generated by orca are currently not reproducible so we just - # check the mime type and that the resulting string is an SVG with the - # expected size - mock_call_args = mock_display.call_args - - mock_arg1 = mock_call_args[0][0] - assert list(mock_arg1) == ['image/svg+xml'] - assert mock_arg1['image/svg+xml'].startswith( - '= 3: + import unittest.mock as mock +else: + import mock + + +# fixtures +# -------- +@pytest.fixture +def fig1(request): + return go.Figure(data=[{'type': 'scatter', + 'marker': {'color': 'green'}}], + layout={'title': {'text': 'Figure title'}}) + + +def test_png_renderer_mimetype(fig1): + pio.renderers.default = 'png' + + # Configure renderer so that we can use the same parameters + # to build expected image below + pio.renderers['png'].width = 400 + pio.renderers['png'].height = 500 + pio.renderers['png'].scale = 1 + + image_bytes = pio.to_image(fig1, width=400, height=500, scale=1) + image_str = base64.b64encode(image_bytes).decode('utf8') + + expected = {'image/png': image_str} + + pio.renderers.render_on_display = False + assert fig1._repr_mimebundle_(None, None) is None + + pio.renderers.render_on_display = True + bundle = fig1._repr_mimebundle_(None, None) + assert bundle == expected + + +def test_svg_renderer_show(fig1): + pio.renderers.default = 'svg' + pio.renderers['svg'].width = 400 + pio.renderers['svg'].height = 500 + pio.renderers['svg'].scale = 1 + + with mock.patch('IPython.display.display') as mock_display: + pio.show(fig1) + + # Check call args. + # SVGs generated by orca are currently not reproducible so we just + # check the mime type and that the resulting string is an SVG with the + # expected size + mock_call_args = mock_display.call_args + + mock_arg1 = mock_call_args[0][0] + assert list(mock_arg1) == ['image/svg+xml'] + assert mock_arg1['image/svg+xml'].startswith( + ' Date: Sat, 23 Mar 2019 08:43:55 -0400 Subject: [PATCH 35/48] Move combination renderers test to orca test suite --- plotly/tests/test_io/test_renderers.py | 38 ----------------- .../tests/test_orca/test_image_renderers.py | 42 +++++++++++++++++++ 2 files changed, 42 insertions(+), 38 deletions(-) diff --git a/plotly/tests/test_io/test_renderers.py b/plotly/tests/test_io/test_renderers.py index 2ba24c68c53..dd3abfe63fa 100644 --- a/plotly/tests/test_io/test_renderers.py +++ b/plotly/tests/test_io/test_renderers.py @@ -253,44 +253,6 @@ def open_url(url, new=0, autoraise=True): assert_not_requirejs(html) -# Combination -# ----------- -def test_mimetype_combination(fig1): - pio.renderers.default = 'pdf+jupyterlab' - - # Configure renderer so that we can use the same parameters - # to build expected image below - pio.renderers['pdf'].width = 400 - pio.renderers['pdf'].height = 500 - pio.renderers['pdf'].scale = 1 - - # pdf - image_bytes = pio.to_image( - fig1, format='pdf', width=400, height=500, scale=1) - - image_str = base64.b64encode(image_bytes).decode('utf8') - - # plotly mimetype - plotly_mimetype_dict = json.loads( - pio.to_json(fig1, remove_uids=False)) - - plotly_mimetype_dict['config'] = { - 'plotlyServerURL': 'https://plot.ly'} - - # Build expected bundle - expected = { - 'application/pdf': image_str, - plotly_mimetype: plotly_mimetype_dict, - } - - pio.renderers.render_on_display = False - assert fig1._repr_mimebundle_(None, None) is None - - pio.renderers.render_on_display = True - bundle = fig1._repr_mimebundle_(None, None) - assert bundle == expected - - # Validation # ---------- @pytest.mark.parametrize( diff --git a/plotly/tests/test_orca/test_image_renderers.py b/plotly/tests/test_orca/test_image_renderers.py index fc0e2f314d1..647521dfb99 100644 --- a/plotly/tests/test_orca/test_image_renderers.py +++ b/plotly/tests/test_orca/test_image_renderers.py @@ -1,16 +1,20 @@ import base64 import sys +import json import pytest from plotly import io as pio import plotly.graph_objs as go +from plotly.config import get_config if sys.version_info.major == 3 and sys.version_info.minor >= 3: import unittest.mock as mock else: import mock +plotly_mimetype = 'application/vnd.plotly.v1+json' + # fixtures # -------- @@ -98,3 +102,41 @@ def test_pdf_renderer_show_override_multi(fig1): 'image/png': image_str_png} mock_display.assert_called_once_with(expected_bundle, raw=True) + + +# Combination +# ----------- +def test_mimetype_combination(fig1): + pio.renderers.default = 'png+jupyterlab' + + # Configure renderer so that we can use the same parameters + # to build expected image below + pio.renderers['png'].width = 400 + pio.renderers['png'].height = 500 + pio.renderers['png'].scale = 1 + + # pdf + image_bytes = pio.to_image( + fig1, format='png', width=400, height=500, scale=1) + + image_str = base64.b64encode(image_bytes).decode('utf8') + + # plotly mimetype + plotly_mimetype_dict = json.loads( + pio.to_json(fig1, remove_uids=False)) + + plotly_mimetype_dict['config'] = { + 'plotlyServerURL': get_config()['plotly_domain']} + + # Build expected bundle + expected = { + 'image/png': image_str, + plotly_mimetype: plotly_mimetype_dict, + } + + pio.renderers.render_on_display = False + assert fig1._repr_mimebundle_(None, None) is None + + pio.renderers.render_on_display = True + bundle = fig1._repr_mimebundle_(None, None) + assert bundle == expected \ No newline at end of file From e8d2f1b4fa0175c4758044920d394971e19b72ba Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 23 Mar 2019 08:59:34 -0400 Subject: [PATCH 36/48] Remove remaining image renderer references from test_io --- plotly/tests/test_io/test_renderers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plotly/tests/test_io/test_renderers.py b/plotly/tests/test_io/test_renderers.py index dd3abfe63fa..f257a89e8c0 100644 --- a/plotly/tests/test_io/test_renderers.py +++ b/plotly/tests/test_io/test_renderers.py @@ -256,7 +256,7 @@ def open_url(url, new=0, autoraise=True): # Validation # ---------- @pytest.mark.parametrize( - 'renderer', ['bogus', 'png+bogus', 'bogus+png']) + 'renderer', ['bogus', 'json+bogus', 'bogus+chrome']) def test_reject_invalid_renderer(renderer): with pytest.raises(ValueError) as e: pio.renderers.default = renderer @@ -265,6 +265,6 @@ def test_reject_invalid_renderer(renderer): @pytest.mark.parametrize( - 'renderer', ['png', 'png+jpg', 'jpg+png+pdf+notebook+json']) + 'renderer', ['json', 'json+firefox', 'chrome+colab+notebook+vscode']) def test_accept_valid_renderer(renderer): pio.renderers.default = renderer From de80ffc988d42799d3a4ba515c50bf4685776e4b Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 23 Mar 2019 09:00:57 -0400 Subject: [PATCH 37/48] Add ipython to orca environment for renderer tests --- .circleci/create_conda_optional_env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/create_conda_optional_env.sh b/.circleci/create_conda_optional_env.sh index ad20809d5fb..8caae9bae72 100755 --- a/.circleci/create_conda_optional_env.sh +++ b/.circleci/create_conda_optional_env.sh @@ -16,7 +16,7 @@ if [ ! -d $HOME/miniconda/envs/circle_optional ]; then # Create environment # PYTHON_VERSION=3.6 $HOME/miniconda/bin/conda create -n circle_optional --yes python=$PYTHON_VERSION \ -requests six pytz retrying psutil pandas decorator pytest mock nose poppler xarray scikit-image +requests six pytz retrying psutil pandas decorator pytest mock nose poppler xarray scikit-image ipython # Install orca into environment $HOME/miniconda/bin/conda install --yes -n circle_optional -c plotly plotly-orca From 7cba3675f774475db29e3b9dfefb5f58f5e697a7 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Sat, 23 Mar 2019 09:14:05 -0400 Subject: [PATCH 38/48] Remove pdf image from renderer tests since it seems to be non-deterministic --- plotly/tests/test_orca/test_image_renderers.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/plotly/tests/test_orca/test_image_renderers.py b/plotly/tests/test_orca/test_image_renderers.py index 647521dfb99..9697042294b 100644 --- a/plotly/tests/test_orca/test_image_renderers.py +++ b/plotly/tests/test_orca/test_image_renderers.py @@ -73,7 +73,7 @@ def test_svg_renderer_show(fig1): assert mock_kwargs == {'raw': True} -def test_pdf_renderer_show_override_multi(fig1): +def test_pdf_renderer_show_override(fig1): pio.renderers.default = None # Configure renderer so that we can use the same parameters @@ -82,24 +82,15 @@ def test_pdf_renderer_show_override_multi(fig1): pio.renderers['png'].height = 500 pio.renderers['png'].scale = 1 - pio.renderers['pdf'].width = 400 - pio.renderers['pdf'].height = 500 - pio.renderers['pdf'].scale = 1 - - image_bytes_pdf = pio.to_image( - fig1, format='pdf', width=400, height=500, scale=1) - image_bytes_png = pio.to_image( fig1, format='png', width=400, height=500, scale=1) - image_str_pdf = base64.b64encode(image_bytes_pdf).decode('utf8') image_str_png = base64.b64encode(image_bytes_png).decode('utf8') with mock.patch('IPython.display.display') as mock_display: - pio.show(fig1, renderer='pdf+png') + pio.show(fig1, renderer='png') - expected_bundle = {'application/pdf': image_str_pdf, - 'image/png': image_str_png} + expected_bundle = {'image/png': image_str_png} mock_display.assert_called_once_with(expected_bundle, raw=True) From 73752d70bbe649a4f70e080c2a7a46555f774994 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 26 Mar 2019 06:18:34 -0400 Subject: [PATCH 39/48] Added chromium browser renderer --- plotly/io/_renderers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index 6a759ed012a..db2caefc26e 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -363,6 +363,7 @@ def show(fig, renderer=None, validate=True, **kwargs): renderers['browser'] = BrowserRenderer(config=config) renderers['firefox'] = BrowserRenderer(config=config, using='firefox') renderers['chrome'] = BrowserRenderer(config=config, using='chrome') +renderers['chromium'] = BrowserRenderer(config=config, using='chromium') renderers['iframe'] = IFrameRenderer(config=config) # Set default renderer From 8a0f6cc5a8ad9571e12e5e8e6709a9d500b1d020 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 26 Mar 2019 06:36:54 -0400 Subject: [PATCH 40/48] Fix plotly mimetype renderer when used with numpy arrays/pandas Series --- plotly/io/_base_renderers.py | 6 +++++- plotly/tests/test_io/test_renderers.py | 2 ++ plotly/tests/test_orca/test_image_renderers.py | 4 +++- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index 823d1142124..90d35e7fee8 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -83,7 +83,11 @@ def to_mimebundle(self, fig_dict): config = _get_jconfig(self.config) if config: fig_dict['config'] = config - return {'application/vnd.plotly.v1+json': fig_dict} + + json_compatible_fig_dict = json.loads( + to_json(fig_dict, validate=False, remove_uids=False)) + + return {'application/vnd.plotly.v1+json': json_compatible_fig_dict} # Static Image diff --git a/plotly/tests/test_io/test_renderers.py b/plotly/tests/test_io/test_renderers.py index f257a89e8c0..f9d7e7452c0 100644 --- a/plotly/tests/test_io/test_renderers.py +++ b/plotly/tests/test_io/test_renderers.py @@ -6,6 +6,7 @@ import pytest import requests +import numpy as np import plotly.graph_objs as go import plotly.io as pio @@ -24,6 +25,7 @@ @pytest.fixture def fig1(request): return go.Figure(data=[{'type': 'scatter', + 'y': np.array([2, 1, 3, 2, 4, 2]), 'marker': {'color': 'green'}}], layout={'title': {'text': 'Figure title'}}) diff --git a/plotly/tests/test_orca/test_image_renderers.py b/plotly/tests/test_orca/test_image_renderers.py index 9697042294b..d04bab8e99b 100644 --- a/plotly/tests/test_orca/test_image_renderers.py +++ b/plotly/tests/test_orca/test_image_renderers.py @@ -3,6 +3,7 @@ import json import pytest +import numpy as np from plotly import io as pio import plotly.graph_objs as go @@ -21,7 +22,8 @@ @pytest.fixture def fig1(request): return go.Figure(data=[{'type': 'scatter', - 'marker': {'color': 'green'}}], + 'marker': {'color': 'green'}, + 'y': np.array([2, 1, 3, 2, 4, 2])}], layout={'title': {'text': 'Figure title'}}) From 504e342a3d0a18b61acf9fa2e50cfd1467d82635 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 26 Mar 2019 08:07:50 -0400 Subject: [PATCH 41/48] Lazily initialize default renderers. This way we can keep notebook_connected as a default renderer, but it won't attempt notebook initialization unless in is explicitly shown. So this won't happen, for example, when using plotly.py in a Dash app --- plotly/io/_renderers.py | 37 +++++++++++++++++++++++--- plotly/tests/test_io/test_renderers.py | 28 ++++++++++--------- 2 files changed, 48 insertions(+), 17 deletions(-) diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index db2caefc26e..4add5cc3cfb 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -15,6 +15,7 @@ SvgRenderer, PdfRenderer, BrowserRenderer, IFrameRenderer) from plotly.io._utils import validate_coerce_fig_to_dict +ipython = optional_imports.get_module('IPython') ipython_display = optional_imports.get_module('IPython.display') @@ -29,6 +30,7 @@ def __init__(self): self._default_name = None self._default_renderers = [] self._render_on_display = False + self._to_activate = [] # ### Magic methods ### # Make this act as a dict of renderers @@ -127,9 +129,8 @@ def default(self, value): self._default_name = value self._default_renderers = [self[name] for name in renderer_names] - # Activate default renderer(s) - for renderer in self._default_renderers: - renderer.activate() + # Register renderers for activation before their next use + self._to_activate.extend(self._default_renderers) @property def render_on_display(self): @@ -150,6 +151,27 @@ def render_on_display(self, val): else: self._render_on_display = False + def _activate_pending_renderers(self, cls=object): + """ + Activate all renderers that are waiting in the _to_activate list + + Parameters + ---------- + cls + Only activate renders that are subclasses of this class + """ + to_activate_with_cls = [r for r in self._to_activate + if cls and isinstance(r, cls)] + + while to_activate_with_cls: + # Activate renderers from left to right so that right-most + # renderers take precedence + renderer = to_activate_with_cls.pop(0) + renderer.activate() + + self._to_activate = [r for r in self._to_activate + if not (cls and isinstance(r, cls))] + def _validate_coerce_renderers(self, renderers_string): """ Input a string and validate that it contains the names of one or more @@ -223,10 +245,14 @@ def _build_mime_bundle(self, fig_dict, renderers_string=None, **kwargs): if renderers_string: renderer_names = self._validate_coerce_renderers(renderers_string) renderers_list = [self[name] for name in renderer_names] + + # Activate these non-default renderers for renderer in renderers_list: if isinstance(renderer, MimetypeRenderer): renderer.activate() else: + # Activate any pending default renderers + self._activate_pending_renderers(cls=MimetypeRenderer) renderers_list = self._default_renderers bundle = {} @@ -266,10 +292,13 @@ def _perform_external_rendering( if renderers_string: renderer_names = self._validate_coerce_renderers(renderers_string) renderers_list = [self[name] for name in renderer_names] + + # Activate these non-default renderers for renderer in renderers_list: if isinstance(renderer, ExternalRenderer): renderer.activate() else: + self._activate_pending_renderers(cls=ExternalRenderer) renderers_list = self._default_renderers for renderer in renderers_list: @@ -374,7 +403,7 @@ def show(fig, renderer=None, validate=True, **kwargs): # Try to detect environment so that we can enable a useful # default renderer - if ipython_display: + if ipython and ipython.get_ipython(): try: import google.colab diff --git a/plotly/tests/test_io/test_renderers.py b/plotly/tests/test_io/test_renderers.py index f9d7e7452c0..6c57073cf1b 100644 --- a/plotly/tests/test_io/test_renderers.py +++ b/plotly/tests/test_io/test_renderers.py @@ -163,24 +163,26 @@ def test_colab_renderer_show(fig1): ('kaggle', True)]) def test_notebook_connected_show(fig1, name, connected): # Set renderer + pio.renderers.default = name + + # Show with mock.patch('IPython.display.display_html') as mock_display_html: - pio.renderers.default = name + with mock.patch('IPython.display.display') as mock_display: + pio.show(fig1) + # ### Check initialization ### # Get display call arguments - mock_call_args = mock_display_html.call_args - mock_arg1 = mock_call_args[0][0] + mock_call_args_html = mock_display_html.call_args + mock_arg1_html = mock_call_args_html[0][0] # Check init display contents - html = mock_arg1 + bundle_display_html = mock_arg1_html if connected: - assert_connected(html) + assert_connected(bundle_display_html) else: - assert_offline(html) - - # Show - with mock.patch('IPython.display.display') as mock_display: - pio.show(fig1) + assert_offline(bundle_display_html) + # ### Check display call ### # Get display call arguments mock_call_args = mock_display.call_args mock_arg1 = mock_call_args[0][0] @@ -189,9 +191,9 @@ def test_notebook_connected_show(fig1, name, connected): assert list(mock_arg1) == ['text/html'] # Check html display contents - html = mock_arg1['text/html'] - assert_not_full_html(html) - assert_requirejs(html) + bundle_html = mock_arg1['text/html'] + assert_not_full_html(bundle_html) + assert_requirejs(bundle_html) # check kwargs mock_kwargs = mock_call_args[1] From 111560805efddc0862bc9b71f141f7ba23c82a20 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 26 Mar 2019 08:13:18 -0400 Subject: [PATCH 42/48] Use 'browser' default renderer if ipython isn't available --- plotly/io/_renderers.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index 4add5cc3cfb..06e8191d523 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -431,6 +431,10 @@ def show(fig, renderer=None, validate=True, **kwargs): # JupyterLab users. if not default_renderer: default_renderer = 'notebook_connected+plotly_mimetype' + else: + # If ipython isn't available, try to display figures in the default + # browser + default_renderer = 'browser' renderers.render_on_display = True renderers.default = default_renderer From c647d70a1fb2d57a4ae34d7f6ea7914ca299c209 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Mon, 8 Apr 2019 19:46:58 -0400 Subject: [PATCH 43/48] Trigger notebook renderer activation on `init_notebook_mode` call This will improve backward compatibility with the legacy init_notebook_mode() function. --- plotly/offline/offline.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plotly/offline/offline.py b/plotly/offline/offline.py index 28a38b3c0c2..5dfbb33e42a 100644 --- a/plotly/offline/offline.py +++ b/plotly/offline/offline.py @@ -290,6 +290,10 @@ def init_notebook_mode(connected=False): else: pio.renderers.default = 'notebook+plotly_mimetype' + # Trigger immediate activation of notebook. This way the plotly.js + # library reference is available to the notebook immediately + pio.renderers._activate_pending_renderers() + def iplot(figure_or_data, show_link=False, link_text='Export to plot.ly', validate=True, image=None, filename='plot_image', image_width=800, From 839f25771ac04b5509a8c70e32c507bec5d835fe Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 9 Apr 2019 06:05:34 -0400 Subject: [PATCH 44/48] Remove whitespace between method calls --- plotly/io/_html.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plotly/io/_html.py b/plotly/io/_html.py index f13eae3be4d..e22e96f4922 100644 --- a/plotly/io/_html.py +++ b/plotly/io/_html.py @@ -172,7 +172,7 @@ def to_html(fig, {layout}, {config} ).then(function () {add_frames})\ - {then_animate}{then_post_script} +{then_animate}{then_post_script} }} '''.format( id=plotdivid, @@ -189,7 +189,7 @@ def to_html(fig, script = """ if (document.getElementById("{id}")) {{ Plotly.newPlot("{id}", {data}, {layout}, {config})\ - {then_post_script} +{then_post_script} }} """.format( id=plotdivid, From 18f36b9934f21b140128244e7c91ccf0924543a4 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 9 Apr 2019 18:21:55 -0400 Subject: [PATCH 45/48] review cleanup --- plotly/__init__.py | 3 ++- plotly/io/_templates.py | 2 ++ plotly/offline/offline.py | 27 ++++----------------------- 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/plotly/__init__.py b/plotly/__init__.py index 8d70b5749df..b9c0170d694 100644 --- a/plotly/__init__.py +++ b/plotly/__init__.py @@ -37,6 +37,7 @@ __version__ = get_versions()['version'] del get_versions -# Set default template here to make sure import process is complet +# Set default template here to make sure import process is complete if 'template_defaults' in _future_flags: + # Set _default to skip validation io.templates._default = 'plotly' diff --git a/plotly/io/_templates.py b/plotly/io/_templates.py index 3fd1377f586..e6c25ce49e1 100644 --- a/plotly/io/_templates.py +++ b/plotly/io/_templates.py @@ -1,4 +1,5 @@ from __future__ import absolute_import + from plotly.basedatatypes import BaseFigure from plotly.graph_objs import Figure from plotly.validators.layout import TemplateValidator @@ -237,6 +238,7 @@ def _merge_2_templates(self, template1, template2): templates = TemplatesConfig() del TemplatesConfig + # Template utilities # ------------------ def walk_push_to_template(fig_obj, template_obj, skip): diff --git a/plotly/offline/offline.py b/plotly/offline/offline.py index 5dfbb33e42a..5cad091f011 100644 --- a/plotly/offline/offline.py +++ b/plotly/offline/offline.py @@ -238,11 +238,8 @@ def build_save_image_post_script( raise ValueError('The image parameter must be one of the ' 'following: {}'.format(__IMAGE_FORMATS) ) - # if the check passes then download script is injected. - # write the download script: - script = get_image_download_script(caller) - # Replace none's with nulls + script = get_image_download_script(caller) post_script = script.format( format=image, width=image_width, @@ -299,11 +296,7 @@ def iplot(figure_or_data, show_link=False, link_text='Export to plot.ly', validate=True, image=None, filename='plot_image', image_width=800, image_height=600, config=None, auto_play=True): """ - Draw plotly graphs inside an IPython or Jupyter notebook without - connecting to an external server. - To save the chart to Plotly Cloud or Plotly Enterprise, use - `plotly.plotly.iplot`. - To embed an image of the chart, use `plotly.image.ishow`. + Draw plotly graphs inside an IPython or Jupyter notebook figure_or_data -- a plotly.graph_objs.Figure or plotly.graph_objs.Data or dict or list that describes a Plotly graph. @@ -324,8 +317,8 @@ def iplot(figure_or_data, show_link=False, link_text='Export to plot.ly', the format of the image to be downloaded, if we choose to download an image. This parameter has a default value of None indicating that no image should be downloaded. Please note: for higher resolution images - and more export options, consider making requests to our image servers. - Type: `help(py.image)` for more details. + and more export options, consider using plotly.io.write_image. See + https://plot.ly/python/static-image-export/ for more details. filename (default='plot') -- Sets the name of the file your image will be saved to. The extension should not be included. image_height (default=600) -- Specifies the height of the image in `px`. @@ -352,12 +345,6 @@ def iplot(figure_or_data, show_link=False, link_text='Export to plot.ly', if not ipython: raise ImportError('`iplot` can only run inside an IPython Notebook.') - # Deprecations - if image: - warnings.warn(""" - Image export using plotly.offline.plot is no longer supported. - Please use plotly.io.write_image instead""", DeprecationWarning) - config = dict(config) if config else {} config.setdefault('showLink', show_link) config.setdefault('linkText', link_text) @@ -508,12 +495,6 @@ def plot(figure_or_data, show_link=False, link_text='Export to plot.ly', "Adding .html to the end of your file.") filename += '.html' - # Deprecations - if image: - warnings.warn(""" -Image export using plotly.offline.plot is no longer supported. - Please use plotly.io.write_image instead""", DeprecationWarning) - # Config config = dict(config) if config else {} config.setdefault('showLink', show_link) From 5c02ded88ede175c797968c6aa7e93d2f39fdb61 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 9 Apr 2019 18:53:22 -0400 Subject: [PATCH 46/48] simplify to_html logic path --- plotly/io/_html.py | 65 +++++++++++++++++++--------------------------- 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/plotly/io/_html.py b/plotly/io/_html.py index e22e96f4922..f70d269f7f7 100644 --- a/plotly/io/_html.py +++ b/plotly/io/_html.py @@ -156,47 +156,34 @@ def to_html(fig, else: then_post_script = '' + then_addframes = '' + then_animate = '' if jframes: + then_addframes = """.then(function(){{ + Plotly.addFrames('{id}', {frames}); + }})""".format(id=plotdivid, frames=jframes) + if auto_play: then_animate = """.then(function(){{ Plotly.animate('{id}'); - }}""".format(id=plotdivid) - else: - then_animate = '' - - script = ''' - if (document.getElementById("{id}")) {{ - Plotly.plot( - '{id}', - {data}, - {layout}, - {config} - ).then(function () {add_frames})\ -{then_animate}{then_post_script} - }} - '''.format( - id=plotdivid, - data=jdata, - layout=jlayout, - config=jconfig, - add_frames="{" + "return Plotly.addFrames('{id}',{frames}".format( - id=plotdivid, frames=jframes - ) + ");}", - then_animate=then_animate, - then_post_script=then_post_script - ) - else: - script = """ + }})""".format(id=plotdivid) + + script = """ if (document.getElementById("{id}")) {{ - Plotly.newPlot("{id}", {data}, {layout}, {config})\ -{then_post_script} - }} - """.format( - id=plotdivid, - data=jdata, - layout=jlayout, - config=jconfig, - then_post_script=then_post_script) + Plotly.newPlot( + '{id}', + {data}, + {layout}, + {config} + ){then_addframes}{then_animate}{then_post_script} + }}""".format( + id=plotdivid, + data=jdata, + layout=jlayout, + config=jconfig, + then_addframes=then_addframes, + then_animate=then_animate, + then_post_script=then_post_script) # ## Handle loading/initializing plotly.js ## include_plotlyjs_orig = include_plotlyjs @@ -265,10 +252,10 @@ def to_html(fig, mathjax_script = '' else: raise ValueError("""\ - Invalid value of type {typ} received as the include_mathjax argument - Received value: {val} +Invalid value of type {typ} received as the include_mathjax argument + Received value: {val} - include_mathjax may be specified as False, 'cdn', or a string ending with '.js' +include_mathjax may be specified as False, 'cdn', or a string ending with '.js' """.format(typ=type(include_mathjax), val=repr(include_mathjax))) plotly_html_div = """\ From 95e27249f773265432a337aac706b0dfbe9edae0 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 9 Apr 2019 19:34:54 -0400 Subject: [PATCH 47/48] Review / cleanup of new renderers --- _plotly_future_/__init__.py | 2 +- plotly/io/_base_renderers.py | 21 ++++++++++----------- plotly/io/_renderers.py | 23 ++++++++++++----------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/_plotly_future_/__init__.py b/_plotly_future_/__init__.py index 0b1b851d34b..f3cc263c507 100644 --- a/_plotly_future_/__init__.py +++ b/_plotly_future_/__init__.py @@ -5,4 +5,4 @@ def _assert_plotly_not_imported(): import sys if 'plotly' in sys.modules: raise ImportError("""\ -The _plotly_future_ module must be import before the plotly module""") +The _plotly_future_ module must be imported before the plotly module""") diff --git a/plotly/io/_base_renderers.py b/plotly/io/_base_renderers.py index 90d35e7fee8..5f82d098f49 100644 --- a/plotly/io/_base_renderers.py +++ b/plotly/io/_base_renderers.py @@ -20,10 +20,13 @@ from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer -class RendererRepr(object): +class BaseRenderer(object): """ - A mixin implementing a simple __repr__ for Renderer classes + Base class for all renderers """ + def activate(self): + pass + def __repr__(self): try: init_sig = inspect.signature(self.__init__) @@ -44,13 +47,10 @@ def __hash__(self): return hash(repr(self)) -class MimetypeRenderer(RendererRepr): +class MimetypeRenderer(BaseRenderer): """ Base class for all mime type renderers """ - def activate(self): - pass - def to_mimebundle(self, fig_dict): raise NotImplementedError() @@ -63,7 +63,8 @@ class JsonRenderer(MimetypeRenderer): mime type: 'application/json' """ def to_mimebundle(self, fig_dict): - value = json.loads(to_json(fig_dict)) + value = json.loads(to_json( + fig_dict, validate=False, remove_uids=False)) return {'application/json': value} @@ -472,7 +473,7 @@ def to_mimebundle(self, fig_dict): return {'text/html': iframe_html} -class ExternalRenderer(RendererRepr): +class ExternalRenderer(BaseRenderer): """ Base class for external renderers. ExternalRenderer subclasses do not display figures inline in a notebook environment, but render @@ -483,8 +484,6 @@ class ExternalRenderer(RendererRepr): Instead, they are invoked when the plotly.io.show function is called on a figure. """ - def activate(self): - pass def render(self, fig): raise NotImplementedError() @@ -494,7 +493,7 @@ def open_html_in_browser(html, using=None, new=0, autoraise=True): """ Display html in a web browser without creating a temp file. - Instantiates a trivial http server and uses the webbrowser modeul to + Instantiates a trivial http server and uses the webbrowser module to open a URL to retrieve html from that server. Parameters diff --git a/plotly/io/_renderers.py b/plotly/io/_renderers.py index 06e8191d523..293f75e9bbf 100644 --- a/plotly/io/_renderers.py +++ b/plotly/io/_renderers.py @@ -92,9 +92,12 @@ def default(self): """ The default renderer, or None if no there is no default - If not None, the default renderer is automatically used to render - figures when they are displayed in a jupyter notebook or when using - the plotly.io.show function + If not None, the default renderer is used to render + figures when the `plotly.io.show` function is called on a Figure. + + If `plotly.io.renderers.render_on_display` is True, then the default + renderer will also be used to display Figures automatically when + displayed in the Jupyter Notebook Multiple renderers may be registered by separating their names with '+' characters. For example, to specify rendering compatible with @@ -103,7 +106,7 @@ def default(self): >>> import plotly.io as pio >>> pio.renderers.default = 'notebook+jupyterlab+pdf' - The names of available templates may be retrieved with: + The names of available renderers may be retrieved with: >>> import plotly.io as pio >>> list(pio.renderers) @@ -146,10 +149,7 @@ def render_on_display(self): @render_on_display.setter def render_on_display(self, val): - if val: - self._render_on_display = True - else: - self._render_on_display = False + self._render_on_display = bool(val) def _activate_pending_renderers(self, cls=object): """ @@ -206,9 +206,9 @@ def __repr__(self): Available renderers: {available} """.format(default=repr(self.default), - available=self._available_templates_str()) + available=self._available_renderers_str()) - def _available_templates_str(self): + def _available_renderers_str(self): """ Return nicely wrapped string representation of all available renderer names @@ -397,8 +397,8 @@ def show(fig, renderer=None, validate=True, **kwargs): # Set default renderer # -------------------- -# This will change to renderers.set_v4_defaults() in version 4.0 if 'renderer_defaults' in _future_flags: + # Version 4 renderer configuration default_renderer = None # Try to detect environment so that we can enable a useful @@ -439,5 +439,6 @@ def show(fig, renderer=None, validate=True, **kwargs): renderers.render_on_display = True renderers.default = default_renderer else: + # Version 3 defaults renderers.render_on_display = False renderers.default = 'plotly_mimetype' From 99769cc0ea144a8c3e023124331618a1525dcad1 Mon Sep 17 00:00:00 2001 From: Jon Mease Date: Tue, 9 Apr 2019 19:55:00 -0400 Subject: [PATCH 48/48] Fix json renderer tests --- plotly/tests/test_io/test_renderers.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plotly/tests/test_io/test_renderers.py b/plotly/tests/test_io/test_renderers.py index 6c57073cf1b..a916523f549 100644 --- a/plotly/tests/test_io/test_renderers.py +++ b/plotly/tests/test_io/test_renderers.py @@ -34,7 +34,8 @@ def fig1(request): # ---- def test_json_renderer_mimetype(fig1): pio.renderers.default = 'json' - expected = {'application/json': json.loads(pio.to_json(fig1))} + expected = {'application/json': json.loads( + pio.to_json(fig1, remove_uids=False))} pio.renderers.render_on_display = False assert fig1._repr_mimebundle_(None, None) is None @@ -46,7 +47,8 @@ def test_json_renderer_mimetype(fig1): def test_json_renderer_show(fig1): pio.renderers.default = 'json' - expected_bundle = {'application/json': json.loads(pio.to_json(fig1))} + expected_bundle = {'application/json': json.loads( + pio.to_json(fig1, remove_uids=False))} with mock.patch('IPython.display.display') as mock_display: pio.show(fig1) @@ -56,7 +58,8 @@ def test_json_renderer_show(fig1): def test_json_renderer_show_override(fig1): pio.renderers.default = 'notebook' - expected_bundle = {'application/json': json.loads(pio.to_json(fig1))} + expected_bundle = {'application/json': json.loads( + pio.to_json(fig1, remove_uids=False))} with mock.patch('IPython.display.display') as mock_display: pio.show(fig1, renderer='json')