diff options
Diffstat (limited to 'venv/lib/python3.8/site-packages/plotly/io')
15 files changed, 5886 insertions, 0 deletions
diff --git a/venv/lib/python3.8/site-packages/plotly/io/__init__.py b/venv/lib/python3.8/site-packages/plotly/io/__init__.py new file mode 100644 index 0000000..87f9c3a --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/__init__.py @@ -0,0 +1,68 @@ +# ruff: noqa: F401 + +from _plotly_utils.importers import relative_import +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from ._kaleido import ( + to_image, + write_image, + write_images, + full_figure_for_development, + ) + from . import orca, kaleido + from . import json + from ._json import to_json, from_json, read_json, write_json + from ._templates import templates, to_templated + from ._html import to_html, write_html + from ._renderers import renderers, show + from . import base_renderers + from ._kaleido import defaults + + __all__ = [ + "to_image", + "write_image", + "write_images", + "orca", + "json", + "to_json", + "from_json", + "read_json", + "write_json", + "templates", + "to_templated", + "to_html", + "write_html", + "renderers", + "show", + "base_renderers", + "full_figure_for_development", + "defaults", + ] +else: + __all__, __getattr__, __dir__ = relative_import( + __name__, + [".orca", ".kaleido", ".json", ".base_renderers"], + [ + "._kaleido.to_image", + "._kaleido.write_image", + "._kaleido.write_images", + "._kaleido.full_figure_for_development", + "._json.to_json", + "._json.from_json", + "._json.read_json", + "._json.write_json", + "._templates.templates", + "._templates.to_templated", + "._html.to_html", + "._html.write_html", + "._renderers.renderers", + "._renderers.show", + "._kaleido.defaults", + ], + ) + + # Set default template (for < 3.7 this is done in ploty/__init__.py) + from plotly.io import templates + + templates._default = "plotly" diff --git a/venv/lib/python3.8/site-packages/plotly/io/_base_renderers.py b/venv/lib/python3.8/site-packages/plotly/io/_base_renderers.py new file mode 100644 index 0000000..b413aee --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/_base_renderers.py @@ -0,0 +1,846 @@ +import base64 +import json +import webbrowser +import inspect +import os +from os.path import isdir + +from plotly import optional_imports +from plotly.io import to_json, to_image, write_image, write_html +from plotly.io._utils import plotly_cdn_url +from plotly.offline.offline import _get_jconfig, get_plotlyjs +from plotly.tools import return_figure_from_figure_or_data + +ipython_display = optional_imports.get_module("IPython.display") +IPython = optional_imports.get_module("IPython") + +try: + from http.server import BaseHTTPRequestHandler, HTTPServer +except ImportError: + # Python 2.7 + from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer + + +class BaseRenderer(object): + """ + Base class for all renderers + """ + + def activate(self): + pass + + def __repr__(self): + 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]) for k in init_args), + doc=self.__doc__, + ) + + def __hash__(self): + # Constructor args fully define uniqueness + return hash(repr(self)) + + +class MimetypeRenderer(BaseRenderer): + """ + Base class for all mime type renderers + """ + + def to_mimebundle(self, fig_dict): + raise NotImplementedError() + + +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, validate=False, remove_uids=False)) + return {"application/json": value} + + +# Plotly mimetype +class PlotlyRenderer(MimetypeRenderer): + """ + Renderer to display figures using the plotly mime type. This renderer is + compatible with VSCode and nteract. + + mime type: 'application/vnd.plotly.v1+json' + """ + + 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 + + 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 +class ImageRenderer(MimetypeRenderer): + """ + Base class for all static image renderers + """ + + def __init__( + self, + mime_type, + b64_encode=False, + format=None, + width=None, + height=None, + scale=None, + engine="auto", + ): + self.mime_type = mime_type + self.b64_encode = b64_encode + self.format = format + self.width = width + self.height = height + self.scale = scale + self.engine = engine + + def to_mimebundle(self, fig_dict): + image_bytes = to_image( + fig_dict, + format=self.format, + width=self.width, + height=self.height, + scale=self.scale, + validate=False, + engine=self.engine, + ) + + 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): + """ + Renderer to display figures as static PNG images. This renderer requires + either the kaleido package or 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, engine="auto"): + super(PngRenderer, self).__init__( + mime_type="image/png", + b64_encode=True, + format="png", + width=width, + height=height, + scale=scale, + engine=engine, + ) + + +class SvgRenderer(ImageRenderer): + """ + Renderer to display figures as static SVG images. This renderer requires + either the kaleido package or 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, engine="auto"): + super(SvgRenderer, self).__init__( + mime_type="image/svg+xml", + b64_encode=False, + format="svg", + width=width, + height=height, + scale=scale, + engine=engine, + ) + + +class JpegRenderer(ImageRenderer): + """ + Renderer to display figures as static JPEG images. This renderer requires + either the kaleido package or 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, engine="auto"): + super(JpegRenderer, self).__init__( + mime_type="image/jpeg", + b64_encode=True, + format="jpg", + width=width, + height=height, + scale=scale, + engine=engine, + ) + + +class PdfRenderer(ImageRenderer): + """ + Renderer to display figures as static PDF images. This renderer requires + either the kaleido package or 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, engine="auto"): + super(PdfRenderer, self).__init__( + mime_type="application/pdf", + b64_encode=True, + format="pdf", + width=width, + height=height, + scale=scale, + engine=engine, + ) + + +# HTML +# Build script to set global PlotlyConfig object. This must execute before +# plotly.js is loaded. +_window_plotly_config = """\ +window.PlotlyConfig = {MathJaxConfig: 'local'};""" + +_mathjax_config = """\ +if (window.MathJax && window.MathJax.Hub && window.MathJax.Hub.Config) {window.MathJax.Hub.Config({SVG: {font: "STIX-Web"}});}""" + + +class HtmlRenderer(MimetypeRenderer): + """ + Base class for all HTML mime type renderers + + mime type: 'text/html' + """ + + def __init__( + self, + connected=False, + full_html=False, + global_init=False, + config=None, + auto_play=False, + post_script=None, + animation_opts=None, + include_plotlyjs=True, + ): + self.config = dict(config) if config else {} + self.auto_play = auto_play + self.connected = connected + self.global_init = global_init + self.full_html = full_html + self.animation_opts = animation_opts + self.post_script = post_script + self.include_plotlyjs = "cdn" if self.connected else include_plotlyjs + + 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 self.connected: + script = """\ + <script type="text/javascript"> + {win_config} + {mathjax_config} + </script> + <script type="module">import \"{plotly_cdn}\"</script> + """.format( + win_config=_window_plotly_config, + mathjax_config=_mathjax_config, + plotly_cdn=plotly_cdn_url().rstrip(".js"), + ) + + else: + # If not connected then we embed a copy of the plotly.js + # library in the notebook + script = """\ + <script type="text/javascript"> + {win_config} + {mathjax_config} + </script> + <script>{script}</script> + """.format( + script=get_plotlyjs(), + win_config=_window_plotly_config, + mathjax_config=_mathjax_config, + ) + + ipython_display.display_html(script, raw=True) + + def to_mimebundle(self, fig_dict): + from plotly.io import to_html + + include_mathjax = "cdn" + + # build post script + post_script = [ + """ +var gd = document.getElementById('{plot_id}'); +var x = new MutationObserver(function (mutations, observer) {{ + var display = window.getComputedStyle(gd).display; + if (!display || display === 'none') {{ + console.log([gd, 'removed!']); + Plotly.purge(gd); + observer.disconnect(); + }} +}}); + +// Listen for the removal of the full notebook cells +var notebookContainer = gd.closest('#notebook-container'); +if (notebookContainer) {{ + x.observe(notebookContainer, {childList: true}); +}} + +// Listen for the clearing of the current output cell +var outputEl = gd.closest('.output'); +if (outputEl) {{ + x.observe(outputEl, {childList: true}); +}} +""" + ] + + # Add user defined post script + if self.post_script: + if not isinstance(self.post_script, (list, tuple)): + post_script.append(self.post_script) + else: + post_script.extend(self.post_script) + + html = to_html( + fig_dict, + config=self.config, + auto_play=self.auto_play, + include_plotlyjs=self.include_plotlyjs, + include_mathjax=include_mathjax, + post_script=post_script, + full_html=self.full_html, + animation_opts=self.animation_opts, + default_width="100%", + default_height=525, + validate=False, + ) + + return {"text/html": html} + + +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, + post_script=None, + animation_opts=None, + include_plotlyjs=False, + ): + super(NotebookRenderer, self).__init__( + connected=connected, + full_html=False, + global_init=True, + config=config, + auto_play=auto_play, + post_script=post_script, + animation_opts=animation_opts, + include_plotlyjs=include_plotlyjs, + ) + + +class KaggleRenderer(HtmlRenderer): + """ + 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, post_script=None, animation_opts=None + ): + super(KaggleRenderer, self).__init__( + connected=True, + full_html=False, + global_init=True, + config=config, + auto_play=auto_play, + post_script=post_script, + animation_opts=animation_opts, + include_plotlyjs=False, + ) + + +class AzureRenderer(HtmlRenderer): + """ + Renderer to display interactive figures in Azure 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 an Azure notebook. + + mime type: 'text/html' + """ + + def __init__( + self, config=None, auto_play=False, post_script=None, animation_opts=None + ): + super(AzureRenderer, self).__init__( + connected=True, + full_html=False, + global_init=True, + config=config, + auto_play=auto_play, + post_script=post_script, + animation_opts=animation_opts, + include_plotlyjs=False, + ) + + +class ColabRenderer(HtmlRenderer): + """ + 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, post_script=None, animation_opts=None + ): + super(ColabRenderer, self).__init__( + connected=True, + full_html=True, + global_init=False, + config=config, + auto_play=auto_play, + post_script=post_script, + animation_opts=animation_opts, + ) + + +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, + post_script=None, + animation_opts=None, + include_plotlyjs=True, + html_directory="iframe_figures", + ): + self.config = config + self.auto_play = auto_play + self.post_script = post_script + self.animation_opts = animation_opts + self.include_plotlyjs = include_plotlyjs + self.html_directory = html_directory + + 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. + iframe_buffer = 20 + layout = fig_dict.get("layout", {}) + + if layout.get("width", False): + iframe_width = str(layout["width"] + iframe_buffer) + "px" + else: + iframe_width = "100%" + + if layout.get("height", False): + iframe_height = layout["height"] + iframe_buffer + else: + iframe_height = str(525 + iframe_buffer) + "px" + + # Build filename using ipython cell number + filename = self.build_filename() + + # Make directory for + try: + os.makedirs(self.html_directory) + except OSError: + if not isdir(self.html_directory): + raise + + write_html( + fig_dict, + filename, + config=self.config, + auto_play=self.auto_play, + include_plotlyjs=self.include_plotlyjs, + include_mathjax="cdn", + auto_open=False, + post_script=self.post_script, + animation_opts=self.animation_opts, + default_width="100%", + default_height=525, + validate=False, + ) + + # Build IFrame + iframe_html = """\ +<iframe + scrolling="no" + width="{width}" + height="{height}" + src="{src}" + frameborder="0" + allowfullscreen +></iframe> +""".format(width=iframe_width, height=iframe_height, src=self.build_url(filename)) + + return {"text/html": iframe_html} + + def build_filename(self): + ip = IPython.get_ipython() if IPython else None + try: + cell_number = list(ip.history_manager.get_tail(1))[0][1] + 1 if ip else 0 + except Exception: + cell_number = 0 + return "{dirname}/figure_{cell_number}.html".format( + dirname=self.html_directory, cell_number=cell_number + ) + + def build_url(self, filename): + return filename + + +class CoCalcRenderer(IFrameRenderer): + _render_count = 0 + + def build_filename(self): + filename = "{dirname}/figure_{render_count}.html".format( + dirname=self.html_directory, render_count=CoCalcRenderer._render_count + ) + + CoCalcRenderer._render_count += 1 + return filename + + def build_url(self, filename): + return "{filename}?fullscreen=kiosk".format(filename=filename) + + +class ExternalRenderer(BaseRenderer): + """ + 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, 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. + """ + + def render(self, fig): + raise NotImplementedError() + + +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 module 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 + """ + if isinstance(html, str): + html = html.encode("utf8") + + browser = None + + if using is None: + browser = webbrowser.get(None) + else: + if not isinstance(using, tuple): + using = (using,) + for browser_key in using: + try: + browser = webbrowser.get(browser_key) + if browser is not None: + break + except webbrowser.Error: + pass + + if browser is None: + raise ValueError("Can't locate a browser with key in " + str(using)) + + 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) + browser.open( + "http://127.0.0.1:%s" % server.server_port, new=new, autoraise=autoraise + ) + + server.handle_request() + + +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 + 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, + using=None, + new=0, + autoraise=True, + post_script=None, + animation_opts=None, + ): + self.config = config + self.auto_play = auto_play + self.using = using + self.new = new + self.autoraise = autoraise + self.post_script = post_script + self.animation_opts = animation_opts + + def render(self, fig_dict): + from plotly.io import to_html + + html = to_html( + fig_dict, + config=self.config, + auto_play=self.auto_play, + include_plotlyjs=True, + include_mathjax="cdn", + post_script=self.post_script, + full_html=True, + animation_opts=self.animation_opts, + default_width="100%", + default_height="100%", + validate=False, + ) + open_html_in_browser(html, self.using, self.new, self.autoraise) + + +class DatabricksRenderer(ExternalRenderer): + def __init__( + self, + config=None, + auto_play=False, + post_script=None, + animation_opts=None, + include_plotlyjs="cdn", + ): + self.config = config + self.auto_play = auto_play + self.post_script = post_script + self.animation_opts = animation_opts + self.include_plotlyjs = include_plotlyjs + self._displayHTML = None + + @property + def displayHTML(self): + import inspect + + if self._displayHTML is None: + for frame in inspect.getouterframes(inspect.currentframe()): + global_names = set(frame.frame.f_globals) + # Check for displayHTML plus a few others to reduce chance of a false + # hit. + if all(v in global_names for v in ["displayHTML", "display", "spark"]): + self._displayHTML = frame.frame.f_globals["displayHTML"] + break + + if self._displayHTML is None: + raise EnvironmentError( + """ +Unable to detect the Databricks displayHTML function. The 'databricks' renderer is only +supported when called from within the Databricks notebook environment.""" + ) + + return self._displayHTML + + def render(self, fig_dict): + from plotly.io import to_html + + html = to_html( + fig_dict, + config=self.config, + auto_play=self.auto_play, + include_plotlyjs=self.include_plotlyjs, + include_mathjax="cdn", + post_script=self.post_script, + full_html=True, + animation_opts=self.animation_opts, + default_width="100%", + default_height="100%", + validate=False, + ) + + # displayHTML is a Databricks notebook built-in function + self.displayHTML(html) + + +class SphinxGalleryHtmlRenderer(HtmlRenderer): + def __init__( + self, + connected=True, + config=None, + auto_play=False, + post_script=None, + animation_opts=None, + ): + super(SphinxGalleryHtmlRenderer, self).__init__( + connected=connected, + full_html=False, + global_init=False, + config=config, + auto_play=auto_play, + post_script=post_script, + animation_opts=animation_opts, + ) + + def to_mimebundle(self, fig_dict): + from plotly.io import to_html + + if self.connected: + include_plotlyjs = "cdn" + include_mathjax = "cdn" + else: + include_plotlyjs = True + include_mathjax = "cdn" + + html = to_html( + fig_dict, + config=self.config, + auto_play=self.auto_play, + include_plotlyjs=include_plotlyjs, + include_mathjax=include_mathjax, + full_html=self.full_html, + animation_opts=self.animation_opts, + default_width="100%", + default_height=525, + validate=False, + ) + + return {"text/html": html} + + +class SphinxGalleryOrcaRenderer(ExternalRenderer): + def render(self, fig_dict): + stack = inspect.stack() + # Name of script from which plot function was called is retrieved + try: + filename = stack[3].filename # let's hope this is robust... + except Exception: # python 2 + filename = stack[3][1] + filename_root, _ = os.path.splitext(filename) + filename_html = filename_root + ".html" + filename_png = filename_root + ".png" + figure = return_figure_from_figure_or_data(fig_dict, True) + _ = write_html(fig_dict, file=filename_html, include_plotlyjs="cdn") + try: + write_image(figure, filename_png) + except (ValueError, ImportError): + raise ImportError( + "orca and psutil are required to use the `sphinx-gallery-orca` renderer. " + "See https://plotly.com/python/static-image-export/ for instructions on " + "how to install orca. Alternatively, you can use the `sphinx-gallery` " + "renderer (note that png thumbnails can only be generated with " + "the `sphinx-gallery-orca` renderer)." + ) diff --git a/venv/lib/python3.8/site-packages/plotly/io/_defaults.py b/venv/lib/python3.8/site-packages/plotly/io/_defaults.py new file mode 100644 index 0000000..c36530c --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/_defaults.py @@ -0,0 +1,19 @@ +# Default settings for image generation + + +class _Defaults(object): + """ + Class to store default settings for image generation. + """ + + def __init__(self): + self.default_format = "png" + self.default_width = 700 + self.default_height = 500 + self.default_scale = 1 + self.mathjax = None + self.topojson = None + self.plotlyjs = None + + +defaults = _Defaults() diff --git a/venv/lib/python3.8/site-packages/plotly/io/_html.py b/venv/lib/python3.8/site-packages/plotly/io/_html.py new file mode 100644 index 0000000..3e7b89c --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/_html.py @@ -0,0 +1,517 @@ +import uuid +from pathlib import Path +import webbrowser +import hashlib +import base64 + +from _plotly_utils.optional_imports import get_module +from plotly.io._utils import validate_coerce_fig_to_dict, plotly_cdn_url +from plotly.offline.offline import _get_jconfig, get_plotlyjs + +_json = get_module("json") + + +def _generate_sri_hash(content): + """Generate SHA256 hash for SRI (Subresource Integrity)""" + if isinstance(content, str): + content = content.encode("utf-8") + sha256_hash = hashlib.sha256(content).digest() + return "sha256-" + base64.b64encode(sha256_hash).decode("utf-8") + + +# Build script to set global PlotlyConfig object. This must execute before +# plotly.js is loaded. +_window_plotly_config = """\ +<script type="text/javascript">\ +window.PlotlyConfig = {MathJaxConfig: 'local'};\ +</script>""" + +_mathjax_config = """\ +<script type="text/javascript">\ +if (window.MathJax && window.MathJax.Hub && window.MathJax.Hub.Config) {window.MathJax.Hub.Config({SVG: {font: "STIX-Web"}});}\ +</script>""" + + +def to_html( + fig, + config=None, + auto_play=True, + include_plotlyjs=True, + include_mathjax=False, + post_script=None, + full_html=True, + animation_opts=None, + default_width="100%", + default_height="100%", + validate=True, + div_id=None, +): + """ + Convert a figure to an HTML string 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. The url used is versioned to match the bundled plotly.js. + 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 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 list or None (default None) + JavaScript snippet(s) to be included in the resulting div just after + plot creation. The string(s) 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 <html> tag. If False, produce a string containing + a single <div> element. + animation_opts: dict or None (default None) + dict of custom animation parameters to be passed to the function + Plotly.animate in Plotly.js. See + https://github.com/plotly/plotly.js/blob/master/src/plots/animation_attributes.js + for available options. Has no effect if the figure does not contain + frames, or auto_play is False. + default_width, default_height: number or str (default '100%') + The default figure width/height to use if the provided figure does not + specify its own layout.width/layout.height property. May be + specified in pixels as an integer (e.g. 500), or as a css width style + string (e.g. '500px', '100%'). + validate: bool (default True) + True if the figure should be validated before being converted to + JSON, False otherwise. + div_id: str (default None) + If provided, this is the value of the id attribute of the div tag. If None, the + id attribute is a UUID. + + Returns + ------- + str + Representation of figure as an HTML div string + """ + from plotly.io.json import to_json_plotly + + # ## Validate figure ## + fig_dict = validate_coerce_fig_to_dict(fig, validate) + + # ## Generate div id ## + plotdivid = div_id or str(uuid.uuid4()) + + # ## Serialize figure ## + jdata = to_json_plotly(fig_dict.get("data", [])) + jlayout = to_json_plotly(fig_dict.get("layout", {})) + + if fig_dict.get("frames", None): + jframes = to_json_plotly(fig_dict.get("frames", [])) + else: + jframes = None + + # ## Serialize figure config ## + config = _get_jconfig(config) + + # Set responsive + config.setdefault("responsive", True) + + # Get div width/height + layout_dict = fig_dict.get("layout", {}) + template_dict = fig_dict.get("layout", {}).get("template", {}).get("layout", {}) + + div_width = layout_dict.get("width", template_dict.get("width", default_width)) + div_height = layout_dict.get("height", template_dict.get("height", default_height)) + + # Add 'px' suffix to numeric widths + try: + float(div_width) + except (ValueError, TypeError): + pass + else: + div_width = str(div_width) + "px" + + try: + float(div_height) + except (ValueError, TypeError): + pass + else: + div_height = str(div_height) + "px" + + # ## Get platform URL ## + if config.get("showLink", False) or config.get("showSendToCloud", False): + # Figure is going to include a Chart Studio link or send-to-cloud button, + # So we need to configure the PLOTLYENV.BASE_URL property + base_url_line = """ + window.PLOTLYENV.BASE_URL='{plotly_platform_url}';\ +""".format(plotly_platform_url=config.get("plotlyServerURL", "https://plot.ly")) + else: + # Figure is not going to include a Chart Studio link or send-to-cloud button, + # In this case we don't want https://plot.ly to show up anywhere in the HTML + # output + config.pop("plotlyServerURL", None) + config.pop("linkText", None) + config.pop("showLink", None) + base_url_line = "" + + # ## Build script body ## + # This is the part that actually calls Plotly.js + + # build post script snippet(s) + then_post_script = "" + if post_script: + if not isinstance(post_script, (list, tuple)): + post_script = [post_script] + for ps in post_script: + then_post_script += """.then(function(){{ + {post_script} + }})""".format(post_script=ps.replace("{plot_id}", plotdivid)) + + then_addframes = "" + then_animate = "" + if jframes: + then_addframes = """.then(function(){{ + Plotly.addFrames('{id}', {frames}); + }})""".format(id=plotdivid, frames=jframes) + + if auto_play: + if animation_opts: + animation_opts_arg = ", " + _json.dumps(animation_opts) + else: + animation_opts_arg = "" + then_animate = """.then(function(){{ + Plotly.animate('{id}', null{animation_opts}); + }})""".format(id=plotdivid, animation_opts=animation_opts_arg) + + # Serialize config dict to JSON + jconfig = _json.dumps(config) + + script = """\ + if (document.getElementById("{id}")) {{\ + 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 + if isinstance(include_plotlyjs, str): + include_plotlyjs = include_plotlyjs.lower() + + # Init and load + load_plotlyjs = "" + + if include_plotlyjs == "cdn": + # Generate SRI hash from the bundled plotly.js content + plotlyjs_content = get_plotlyjs() + sri_hash = _generate_sri_hash(plotlyjs_content) + + load_plotlyjs = """\ + {win_config} + <script charset="utf-8" src="{cdn_url}" integrity="{integrity}" crossorigin="anonymous"></script>\ + """.format( + win_config=_window_plotly_config, + cdn_url=plotly_cdn_url(), + integrity=sri_hash, + ) + + elif include_plotlyjs == "directory": + load_plotlyjs = """\ + {win_config} + <script charset="utf-8" src="plotly.min.js"></script>\ + """.format(win_config=_window_plotly_config) + + elif isinstance(include_plotlyjs, str) and include_plotlyjs.endswith(".js"): + load_plotlyjs = """\ + {win_config} + <script charset="utf-8" src="{url}"></script>\ + """.format(win_config=_window_plotly_config, url=include_plotlyjs_orig) + + elif include_plotlyjs: + load_plotlyjs = """\ + {win_config} + <script type="text/javascript">{plotlyjs}</script>\ + """.format(win_config=_window_plotly_config, plotlyjs=get_plotlyjs()) + + # ## Handle loading/initializing MathJax ## + include_mathjax_orig = include_mathjax + if isinstance(include_mathjax, str): + include_mathjax = include_mathjax.lower() + + mathjax_template = """\ + <script src="{url}?config=TeX-AMS-MML_SVG"></script>""" + + if include_mathjax == "cdn": + mathjax_script = ( + mathjax_template.format( + url=("https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js") + ) + + _mathjax_config + ) + + elif isinstance(include_mathjax, str) and include_mathjax.endswith(".js"): + mathjax_script = ( + mathjax_template.format(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)) + ) + + plotly_html_div = """\ +<div>\ + {mathjax_script}\ + {load_plotlyjs}\ + <div id="{id}" class="plotly-graph-div" \ +style="height:{height}; width:{width};"></div>\ + <script type="text/javascript">\ + window.PLOTLYENV=window.PLOTLYENV || {{}};{base_url_line}\ + {script};\ + </script>\ + </div>""".format( + mathjax_script=mathjax_script, + load_plotlyjs=load_plotlyjs, + id=plotdivid, + width=div_width, + height=div_height, + base_url_line=base_url_line, + script=script, + ).strip() + + if full_html: + return """\ +<html> +<head><meta charset="utf-8" /></head> +<body> + {div} +</body> +</html>""".format(div=plotly_html_div) + else: + return plotly_html_div + + +def write_html( + fig, + file, + config=None, + auto_play=True, + include_plotlyjs=True, + include_mathjax=False, + post_script=None, + full_html=True, + animation_opts=None, + validate=True, + default_width="100%", + default_height="100%", + auto_open=False, + div_id=None, +): + """ + 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. a pathlib.Path object or 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. The url used is versioned to match the bundled plotly.js. + 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 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 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 list or None (default None) + JavaScript snippet(s) to be included in the resulting div just after + plot creation. The string(s) 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 <html> tag. If False, produce a string containing + a single <div> element. + animation_opts: dict or None (default None) + dict of custom animation parameters to be passed to the function + Plotly.animate in Plotly.js. See + https://github.com/plotly/plotly.js/blob/master/src/plots/animation_attributes.js + for available options. Has no effect if the figure does not contain + frames, or auto_play is False. + default_width, default_height: number or str (default '100%') + The default figure width/height to use if the provided figure does not + specify its own layout.width/layout.height property. May be + specified in pixels as an integer (e.g. 500), or as a css width style + string (e.g. '500px', '100%'). + 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. + div_id: str (default None) + If provided, this is the value of the id attribute of the div tag. If None, the + id attribute is a UUID. + + Returns + ------- + None + """ + + # Build HTML string + html_str = to_html( + fig, + config=config, + auto_play=auto_play, + include_plotlyjs=include_plotlyjs, + include_mathjax=include_mathjax, + post_script=post_script, + full_html=full_html, + animation_opts=animation_opts, + default_width=default_width, + default_height=default_height, + validate=validate, + div_id=div_id, + ) + + # Check if file is a string + if isinstance(file, str): + # Use the standard pathlib constructor to make a pathlib object. + path = Path(file) + elif isinstance(file, Path): # PurePath is the most general pathlib object. + # `file` is already a pathlib object. + path = file + else: + # We could not make a pathlib object out of file. Either `file` is an open file + # descriptor with a `write()` method or it's an invalid object. + path = None + + # Write HTML string + if path is not None: + # To use a different file encoding, pass a file descriptor + path.write_text(html_str, "utf-8") + else: + file.write(html_str) + + # Check if we should copy plotly.min.js to output directory + if path is not None and full_html and include_plotlyjs == "directory": + bundle_path = path.parent / "plotly.min.js" + + if not bundle_path.exists(): + bundle_path.write_text(get_plotlyjs(), encoding="utf-8") + + # Handle auto_open + if path is not None and full_html and auto_open: + url = path.absolute().as_uri() + webbrowser.open(url) diff --git a/venv/lib/python3.8/site-packages/plotly/io/_json.py b/venv/lib/python3.8/site-packages/plotly/io/_json.py new file mode 100644 index 0000000..e4324d1 --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/_json.py @@ -0,0 +1,594 @@ +import json +import decimal +import datetime +import warnings +from pathlib import Path + +from plotly.io._utils import validate_coerce_fig_to_dict, validate_coerce_output_type +from _plotly_utils.optional_imports import get_module +from _plotly_utils.basevalidators import ImageUriValidator + + +# Orca configuration class +# ------------------------ +class JsonConfig(object): + _valid_engines = ("json", "orjson", "auto") + + def __init__(self): + self._default_engine = "auto" + + @property + def default_engine(self): + return self._default_engine + + @default_engine.setter + def default_engine(self, val): + if val not in JsonConfig._valid_engines: + raise ValueError( + "Supported JSON engines include {valid}\n Received {val}".format( + valid=JsonConfig._valid_engines, val=val + ) + ) + + if val == "orjson": + self.validate_orjson() + + self._default_engine = val + + @classmethod + def validate_orjson(cls): + orjson = get_module("orjson") + if orjson is None: + raise ValueError("The orjson engine requires the orjson package") + + +config = JsonConfig() + + +def coerce_to_strict(const): + """ + This is used to ultimately *encode* into strict JSON, see `encode` + + """ + # before python 2.7, 'true', 'false', 'null', were include here. + if const in ("Infinity", "-Infinity", "NaN"): + return None + else: + return const + + +_swap_json = ( + ("<", "\\u003c"), + (">", "\\u003e"), + ("/", "\\u002f"), +) +_swap_orjson = _swap_json + ( + ("\u2028", "\\u2028"), + ("\u2029", "\\u2029"), +) + + +def _safe(json_str, _swap): + out = json_str + for unsafe_char, safe_char in _swap: + if unsafe_char in out: + out = out.replace(unsafe_char, safe_char) + return out + + +def to_json_plotly(plotly_object, pretty=False, engine=None): + """ + Convert a plotly/Dash object to a JSON string representation + + Parameters + ---------- + plotly_object: + A plotly/Dash object represented as a dict, graph_object, or Dash component + + pretty: bool (default False) + True if JSON representation should be pretty-printed, False if + representation should be as compact as possible. + + engine: str (default None) + The JSON encoding engine to use. One of: + - "json" for an engine based on the built-in Python json module + - "orjson" for a faster engine that requires the orjson package + - "auto" for the "orjson" engine if available, otherwise "json" + If not specified, the default engine is set to the current value of + plotly.io.json.config.default_engine. + + Returns + ------- + str + Representation of input object as a JSON string + + See Also + -------- + to_json : Convert a plotly Figure to JSON with validation + """ + orjson = get_module("orjson", should_load=True) + + # Determine json engine + if engine is None: + engine = config.default_engine + + if engine == "auto": + if orjson is not None: + engine = "orjson" + else: + engine = "json" + elif engine not in ["orjson", "json"]: + raise ValueError("Invalid json engine: %s" % engine) + + modules = { + "sage_all": get_module("sage.all", should_load=False), + "np": get_module("numpy", should_load=False), + "pd": get_module("pandas", should_load=False), + "image": get_module("PIL.Image", should_load=False), + } + + # Dump to a JSON string and return + # -------------------------------- + if engine == "json": + opts = {} + if pretty: + opts["indent"] = 2 + else: + # Remove all whitespace + opts["separators"] = (",", ":") + + from _plotly_utils.utils import PlotlyJSONEncoder + + return _safe( + json.dumps(plotly_object, cls=PlotlyJSONEncoder, **opts), _swap_json + ) + elif engine == "orjson": + JsonConfig.validate_orjson() + opts = orjson.OPT_NON_STR_KEYS | orjson.OPT_SERIALIZE_NUMPY + + if pretty: + opts |= orjson.OPT_INDENT_2 + + # Plotly + try: + plotly_object = plotly_object.to_plotly_json() + except AttributeError: + pass + + # Try without cleaning + try: + return _safe( + orjson.dumps(plotly_object, option=opts).decode("utf8"), _swap_orjson + ) + except TypeError: + pass + + cleaned = clean_to_json_compatible( + plotly_object, + numpy_allowed=True, + datetime_allowed=True, + modules=modules, + ) + return _safe(orjson.dumps(cleaned, option=opts).decode("utf8"), _swap_orjson) + + +def to_json(fig, validate=True, pretty=False, remove_uids=True, engine=None): + """ + Convert a figure to a JSON string representation + + Parameters + ---------- + fig: + Figure object or dict representing a figure + + validate: bool (default True) + True if the figure should be validated before being converted to + JSON, False otherwise. + + pretty: bool (default False) + True if JSON representation should be pretty-printed, False if + representation should be as compact as possible. + + remove_uids: bool (default True) + True if trace UIDs should be omitted from the JSON representation + + engine: str (default None) + The JSON encoding engine to use. One of: + - "json" for an engine based on the built-in Python json module + - "orjson" for a faster engine that requires the orjson package + - "auto" for the "orjson" engine if available, otherwise "json" + If not specified, the default engine is set to the current value of + plotly.io.json.config.default_engine. + + Returns + ------- + str + Representation of figure as a JSON string + + See Also + -------- + to_json_plotly : Convert an arbitrary plotly graph_object or Dash component to JSON + """ + # Validate figure + # --------------- + fig_dict = validate_coerce_fig_to_dict(fig, validate) + + # Remove trace uid + # ---------------- + if remove_uids: + for trace in fig_dict.get("data", []): + trace.pop("uid", None) + + return to_json_plotly(fig_dict, pretty=pretty, engine=engine) + + +def write_json(fig, file, validate=True, pretty=False, remove_uids=True, engine=None): + """ + Convert a figure to JSON and write it to a file or writeable + object + + 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. a pathlib.Path object or an open file descriptor) + + pretty: bool (default False) + True if JSON representation should be pretty-printed, False if + representation should be as compact as possible. + + remove_uids: bool (default True) + True if trace UIDs should be omitted from the JSON representation + + engine: str (default None) + The JSON encoding engine to use. One of: + - "json" for an engine based on the built-in Python json module + - "orjson" for a faster engine that requires the orjson package + - "auto" for the "orjson" engine if available, otherwise "json" + If not specified, the default engine is set to the current value of + plotly.io.json.config.default_engine. + Returns + ------- + None + """ + + # Get JSON string + # --------------- + # Pass through validate argument and let to_json handle validation logic + json_str = to_json( + fig, validate=validate, pretty=pretty, remove_uids=remove_uids, engine=engine + ) + + # Try to cast `file` as a pathlib object `path`. + # ---------------------------------------------- + if isinstance(file, str): + # Use the standard Path constructor to make a pathlib object. + path = Path(file) + elif isinstance(file, Path): + # `file` is already a Path object. + path = file + else: + # We could not make a Path object out of file. Either `file` is an open file + # descriptor with a `write()` method or it's an invalid object. + path = None + + # Open file + # --------- + if path is None: + # We previously failed to make sense of `file` as a pathlib object. + # Attempt to write to `file` as an open file descriptor. + try: + file.write(json_str) + return + except AttributeError: + pass + raise ValueError( + """ +The 'file' argument '{file}' is not a string, pathlib.Path object, or file descriptor. +""".format(file=file) + ) + else: + # We previously succeeded in interpreting `file` as a pathlib object. + # Now we can use `write_bytes()`. + path.write_text(json_str) + + +def from_json_plotly(value, engine=None): + """ + Parse JSON string using the specified JSON engine + + Parameters + ---------- + value: str or bytes + A JSON string or bytes object + + engine: str (default None) + The JSON decoding engine to use. One of: + - if "json", parse JSON using built in json module + - if "orjson", parse using the faster orjson module, requires the orjson + package + - if "auto" use orjson module if available, otherwise use the json module + + If not specified, the default engine is set to the current value of + plotly.io.json.config.default_engine. + + Returns + ------- + dict + + See Also + -------- + from_json_plotly : Parse JSON with plotly conventions into a dict + """ + orjson = get_module("orjson", should_load=True) + + # Validate value + # -------------- + if not isinstance(value, (str, bytes)): + raise ValueError( + """ +from_json_plotly requires a string or bytes argument but received value of type {typ} + Received value: {value}""".format(typ=type(value), value=value) + ) + + # Determine json engine + if engine is None: + engine = config.default_engine + + if engine == "auto": + if orjson is not None: + engine = "orjson" + else: + engine = "json" + elif engine not in ["orjson", "json"]: + raise ValueError("Invalid json engine: %s" % engine) + + if engine == "orjson": + JsonConfig.validate_orjson() + # orjson handles bytes input natively + value_dict = orjson.loads(value) + else: + # decode bytes to str for built-in json module + if isinstance(value, bytes): + value = value.decode("utf-8") + value_dict = json.loads(value) + + return value_dict + + +def from_json(value, output_type="Figure", skip_invalid=False, engine=None): + """ + Construct a figure from a JSON string + + Parameters + ---------- + value: str or bytes + String or bytes object containing the JSON representation of a figure + + output_type: type or str (default 'Figure') + The output figure type or type name. + One of: graph_objs.Figure, 'Figure', graph_objs.FigureWidget, 'FigureWidget' + + skip_invalid: bool (default False) + False if invalid figure properties should result in an exception. + True if invalid figure properties should be silently ignored. + + engine: str (default None) + The JSON decoding engine to use. One of: + - if "json", parse JSON using built in json module + - if "orjson", parse using the faster orjson module, requires the orjson + package + - if "auto" use orjson module if available, otherwise use the json module + + If not specified, the default engine is set to the current value of + plotly.io.json.config.default_engine. + + Raises + ------ + ValueError + if value is not a string, or if skip_invalid=False and value contains + invalid figure properties + + Returns + ------- + Figure or FigureWidget + """ + + # Decode JSON + # ----------- + fig_dict = from_json_plotly(value, engine=engine) + + # Validate coerce output type + # --------------------------- + cls = validate_coerce_output_type(output_type) + + # Create and return figure + # ------------------------ + fig = cls(fig_dict, skip_invalid=skip_invalid) + return fig + + +def read_json(file, output_type="Figure", skip_invalid=False, engine=None): + """ + Construct a figure from the JSON contents of a local file or readable + Python object + + Parameters + ---------- + file: str or readable + A string containing the path to a local file or a read-able Python + object (e.g. a pathlib.Path object or an open file descriptor) + + output_type: type or str (default 'Figure') + The output figure type or type name. + One of: graph_objs.Figure, 'Figure', graph_objs.FigureWidget, 'FigureWidget' + + skip_invalid: bool (default False) + False if invalid figure properties should result in an exception. + True if invalid figure properties should be silently ignored. + + engine: str (default None) + The JSON decoding engine to use. One of: + - if "json", parse JSON using built in json module + - if "orjson", parse using the faster orjson module, requires the orjson + package + - if "auto" use orjson module if available, otherwise use the json module + + If not specified, the default engine is set to the current value of + plotly.io.json.config.default_engine. + + Returns + ------- + Figure or FigureWidget + """ + + # Try to cast `file` as a pathlib object `path`. + if isinstance(file, str): + # Use the standard Path constructor to make a pathlib object. + path = Path(file) + elif isinstance(file, Path): + # `file` is already a Path object. + path = file + else: + # We could not make a Path object out of file. Either `file` is an open file + # descriptor with a `write()` method or it's an invalid object. + path = None + + # Read file contents into JSON string + # ----------------------------------- + if path is not None: + json_str = path.read_text() + else: + json_str = file.read() + + # Construct and return figure + # --------------------------- + return from_json( + json_str, skip_invalid=skip_invalid, output_type=output_type, engine=engine + ) + + +def clean_to_json_compatible(obj, **kwargs): + # Try handling value as a scalar value that we have a conversion for. + # Return immediately if we know we've hit a primitive value + + # Bail out fast for simple scalar types + if isinstance(obj, (int, float, str)): + return obj + + if isinstance(obj, dict): + return {k: clean_to_json_compatible(v, **kwargs) for k, v in obj.items()} + elif isinstance(obj, (list, tuple)): + if obj: + # Must process list recursively even though it may be slow + return [clean_to_json_compatible(v, **kwargs) for v in obj] + + # unpack kwargs + numpy_allowed = kwargs.get("numpy_allowed", False) + datetime_allowed = kwargs.get("datetime_allowed", False) + + modules = kwargs.get("modules", {}) + sage_all = modules["sage_all"] + np = modules["np"] + pd = modules["pd"] + image = modules["image"] + + # Sage + if sage_all is not None: + if obj in sage_all.RR: + return float(obj) + elif obj in sage_all.ZZ: + return int(obj) + + # numpy + if np is not None: + if obj is np.ma.core.masked: + return float("nan") + elif isinstance(obj, np.ndarray): + if numpy_allowed and obj.dtype.kind in ("b", "i", "u", "f"): + return np.ascontiguousarray(obj) + elif obj.dtype.kind == "M": + # datetime64 array + return np.datetime_as_string(obj).tolist() + elif obj.dtype.kind == "U": + return obj.tolist() + elif obj.dtype.kind == "O": + # Treat object array as a lists, continue processing + obj = obj.tolist() + elif isinstance(obj, np.datetime64): + return str(obj) + + # pandas + if pd is not None: + if obj is pd.NaT or obj is pd.NA: + return None + elif isinstance(obj, (pd.Series, pd.DatetimeIndex)): + if numpy_allowed and obj.dtype.kind in ("b", "i", "u", "f"): + return np.ascontiguousarray(obj.values) + elif obj.dtype.kind == "M": + if isinstance(obj, pd.Series): + with warnings.catch_warnings(): + warnings.simplefilter("ignore", FutureWarning) + # Series.dt.to_pydatetime will return Index[object] + # https://github.com/pandas-dev/pandas/pull/52459 + dt_values = np.array(obj.dt.to_pydatetime()).tolist() + else: # DatetimeIndex + dt_values = obj.to_pydatetime().tolist() + + if not datetime_allowed: + # Note: We don't need to handle dropping timezones here because + # numpy's datetime64 doesn't support them and pandas's tz_localize + # above drops them. + for i in range(len(dt_values)): + dt_values[i] = dt_values[i].isoformat() + + return dt_values + + # datetime and date + try: + # Need to drop timezone for scalar datetimes. Don't need to convert + # to string since engine can do that + obj = obj.to_pydatetime() + except (TypeError, AttributeError): + pass + + if not datetime_allowed: + try: + return obj.isoformat() + except (TypeError, AttributeError): + pass + elif isinstance(obj, datetime.datetime): + return obj + + # Try .tolist() convertible, do not recurse inside + try: + return obj.tolist() + except AttributeError: + pass + + # Do best we can with decimal + if isinstance(obj, decimal.Decimal): + return float(obj) + + # PIL + if image is not None and isinstance(obj, image.Image): + return ImageUriValidator.pil_image_to_uri(obj) + + # Plotly + try: + obj = obj.to_plotly_json() + except AttributeError: + pass + + # Recurse into lists and dictionaries + if isinstance(obj, dict): + return {k: clean_to_json_compatible(v, **kwargs) for k, v in obj.items()} + elif isinstance(obj, (list, tuple)): + if obj: + # Must process list recursively even though it may be slow + return [clean_to_json_compatible(v, **kwargs) for v in obj] + + return obj diff --git a/venv/lib/python3.8/site-packages/plotly/io/_kaleido.py b/venv/lib/python3.8/site-packages/plotly/io/_kaleido.py new file mode 100644 index 0000000..6775c33 --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/_kaleido.py @@ -0,0 +1,872 @@ +import os +import json +from pathlib import Path +from typing import Union, List +import importlib.metadata as importlib_metadata +from packaging.version import Version +import warnings + +import plotly +from plotly.io._utils import validate_coerce_fig_to_dict, broadcast_args_to_dicts +from plotly.io._defaults import defaults + +ENGINE_SUPPORT_TIMELINE = "September 2025" +ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS = True + +PLOTLY_GET_CHROME_ERROR_MSG = """ + +Kaleido requires Google Chrome to be installed. + +Either download and install Chrome yourself following Google's instructions for your operating system, +or install it from your terminal by running: + + $ plotly_get_chrome + +""" + +KALEIDO_DEPRECATION_MSG = f""" +Support for Kaleido versions less than 1.0.0 is deprecated and will be removed after {ENGINE_SUPPORT_TIMELINE}. +Please upgrade Kaleido to version 1.0.0 or greater (`pip install 'kaleido>=1.0.0'` or `pip install 'plotly[kaleido]'`). +""" +ORCA_DEPRECATION_MSG = f""" +Support for the Orca engine is deprecated and will be removed after {ENGINE_SUPPORT_TIMELINE}. +Please install Kaleido (`pip install 'kaleido>=1.0.0'` or `pip install 'plotly[kaleido]'`) to use the Kaleido engine. +""" +ENGINE_PARAM_DEPRECATION_MSG = f""" +Support for the 'engine' argument is deprecated and will be removed after {ENGINE_SUPPORT_TIMELINE}. +Kaleido will be the only supported engine at that time. +""" + +_KALEIDO_AVAILABLE = None +_KALEIDO_MAJOR = None + + +def kaleido_scope_default_warning_func(x): + return f""" +Use of plotly.io.kaleido.scope.{x} is deprecated and support will be removed after {ENGINE_SUPPORT_TIMELINE}. +Please use plotly.io.defaults.{x} instead. +""" + + +def bad_attribute_error_msg_func(x): + return f""" +Attribute plotly.io.defaults.{x} is not valid. +Also, use of plotly.io.kaleido.scope.* is deprecated and support will be removed after {ENGINE_SUPPORT_TIMELINE}. +Please use plotly.io.defaults.* instead. +""" + + +def kaleido_available() -> bool: + """ + Returns True if any version of Kaleido is installed, otherwise False. + """ + global _KALEIDO_AVAILABLE + global _KALEIDO_MAJOR + if _KALEIDO_AVAILABLE is not None: + return _KALEIDO_AVAILABLE + try: + import kaleido # noqa: F401 + + _KALEIDO_AVAILABLE = True + except ImportError: + _KALEIDO_AVAILABLE = False + return _KALEIDO_AVAILABLE + + +def kaleido_major() -> int: + """ + Returns the major version number of Kaleido if it is installed, + otherwise raises a ValueError. + """ + global _KALEIDO_MAJOR + if _KALEIDO_MAJOR is not None: + return _KALEIDO_MAJOR + if not kaleido_available(): + raise ValueError("Kaleido is not installed.") + else: + _KALEIDO_MAJOR = Version(importlib_metadata.version("kaleido")).major + return _KALEIDO_MAJOR + + +try: + if kaleido_available() and kaleido_major() < 1: + # Kaleido v0 + import kaleido + from kaleido.scopes.plotly import PlotlyScope + + # Show a deprecation warning if the old method of setting defaults is used + class PlotlyScopeWrapper(PlotlyScope): + def __setattr__(self, name, value): + if name in defaults.__dict__: + if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: + warnings.warn( + kaleido_scope_default_warning_func(name), + DeprecationWarning, + stacklevel=2, + ) + super().__setattr__(name, value) + + def __getattr__(self, name): + if hasattr(defaults, name): + if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: + warnings.warn( + kaleido_scope_default_warning_func(name), + DeprecationWarning, + stacklevel=2, + ) + return super().__getattr__(name) + + # Ensure the new method of setting defaults is backwards compatible with Kaleido v0 + # DefaultsBackwardsCompatible sets the attributes on `scope` object at the same time + # as they are set on the `defaults` object + class DefaultsBackwardsCompatible(defaults.__class__): + def __init__(self, scope): + self._scope = scope + super().__init__() + + def __setattr__(self, name, value): + if not name == "_scope": + if ( + hasattr(self._scope, name) + and getattr(self._scope, name) != value + ): + setattr(self._scope, name, value) + super().__setattr__(name, value) + + scope = PlotlyScopeWrapper() + defaults = DefaultsBackwardsCompatible(scope) + # Compute absolute path to the 'plotly/package_data/' directory + root_dir = os.path.dirname(os.path.abspath(plotly.__file__)) + package_dir = os.path.join(root_dir, "package_data") + scope.plotlyjs = os.path.join(package_dir, "plotly.min.js") + if scope.mathjax is None: + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", message=r".*scope\.mathjax.*", category=DeprecationWarning + ) + scope.mathjax = ( + "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js" + ) + else: + # Kaleido v1 + import kaleido + + # Show a deprecation warning if the old method of setting defaults is used + class DefaultsWrapper: + def __getattr__(self, name): + if hasattr(defaults, name): + if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: + warnings.warn( + kaleido_scope_default_warning_func(name), + DeprecationWarning, + stacklevel=2, + ) + return getattr(defaults, name) + else: + raise AttributeError(bad_attribute_error_msg_func(name)) + + def __setattr__(self, name, value): + if hasattr(defaults, name): + if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: + warnings.warn( + kaleido_scope_default_warning_func(name), + DeprecationWarning, + stacklevel=2, + ) + setattr(defaults, name, value) + else: + raise AttributeError(bad_attribute_error_msg_func(name)) + + scope = DefaultsWrapper() + +except ImportError: + PlotlyScope = None + scope = None + + +def as_path_object(file: Union[str, Path]) -> Union[Path, None]: + """ + Cast the `file` argument, which may be either a string or a Path object, + to a Path object. + If `file` is neither a string nor a Path object, None will be returned. + """ + if isinstance(file, str): + # Use the standard Path constructor to make a pathlib object. + path = Path(file) + elif isinstance(file, Path): + # `file` is already a Path object. + path = file + else: + # We could not make a Path object out of file. Either `file` is an open file + # descriptor with a `write()` method or it's an invalid object. + path = None + return path + + +def infer_format(path: Union[Path, None], format: Union[str, None]) -> Union[str, None]: + if path is not None and format is None: + ext = path.suffix + if ext: + format = ext.lstrip(".") + else: + raise ValueError( + f""" +Cannot infer image type from output path '{path}'. +Please specify the type using the format parameter, or add a file extension. +For example: + + >>> import plotly.io as pio + >>> pio.write_image(fig, file_path, format='png') +""" + ) + return format + + +def to_image( + fig: Union[dict, plotly.graph_objects.Figure], + format: Union[str, None] = None, + width: Union[int, None] = None, + height: Union[int, None] = None, + scale: Union[int, float, None] = None, + validate: bool = True, + # Deprecated + engine: Union[str, None] = None, +) -> bytes: + """ + Convert a figure to a static image bytes string + + Parameters + ---------- + fig: + Figure object or dict representing a figure + + format: str or None + The desired image format. One of + - 'png' + - 'jpg' or 'jpeg' + - 'webp' + - 'svg' + - 'pdf' + - 'eps' (deprecated) (Requires the poppler library to be installed and on the PATH) + + If not specified, will default to: + - `plotly.io.defaults.default_format` if engine is "kaleido" + - `plotly.io.orca.config.default_format` if engine is "orca" (deprecated) + + width: int or None + The width of the exported image in layout pixels. If the `scale` + property is 1.0, this will also be the width of the exported image + in physical pixels. + + If not specified, will default to: + - `plotly.io.defaults.default_width` if engine is "kaleido" + - `plotly.io.orca.config.default_width` if engine is "orca" (deprecated) + + height: int or None + The height of the exported image in layout pixels. If the `scale` + property is 1.0, this will also be the height of the exported image + in physical pixels. + + If not specified, will default to: + - `plotly.io.defaults.default_height` if engine is "kaleido" + - `plotly.io.orca.config.default_height` if engine is "orca" (deprecated) + + scale: int or float or None + The scale factor to use when exporting the figure. A scale factor + larger than 1.0 will increase the image resolution with respect + to the figure's layout pixel dimensions. Whereas as scale factor of + less than 1.0 will decrease the image resolution. + + If not specified, will default to: + - `plotly.io.defaults.default_scale` if engine is "kaleido" + - `plotly.io.orca.config.default_scale` if engine is "orca" (deprecated) + + validate: bool + True if the figure should be validated before being converted to + an image, False otherwise. + + engine (deprecated): str + Image export engine to use. This parameter is deprecated and Orca engine support will be + dropped in the next major Plotly version. Until then, the following values are supported: + - "kaleido": Use Kaleido for image export + - "orca": Use Orca for image export + - "auto" (default): Use Kaleido if installed, otherwise use Orca + + Returns + ------- + bytes + The image data + """ + + # Handle engine + if engine is not None: + if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: + warnings.warn( + ENGINE_PARAM_DEPRECATION_MSG, DeprecationWarning, stacklevel=2 + ) + else: + engine = "auto" + + if engine == "auto": + if kaleido_available(): + # Default to kaleido if available + engine = "kaleido" + else: + # See if orca is available + from ._orca import validate_executable + + try: + validate_executable() + engine = "orca" + except Exception: + # If orca not configured properly, make sure we display the error + # message advising the installation of kaleido + engine = "kaleido" + + if engine == "orca": + if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: + warnings.warn(ORCA_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) + # Fall back to legacy orca image export path + from ._orca import to_image as to_image_orca + + return to_image_orca( + fig, + format=format, + width=width, + height=height, + scale=scale, + validate=validate, + ) + elif engine != "kaleido": + raise ValueError(f"Invalid image export engine specified: {repr(engine)}") + + # Raise informative error message if Kaleido is not installed + if not kaleido_available(): + raise ValueError( + """ +Image export using the "kaleido" engine requires the Kaleido package, +which can be installed using pip: + + $ pip install --upgrade kaleido +""" + ) + + # Convert figure to dict (and validate if requested) + fig_dict = validate_coerce_fig_to_dict(fig, validate) + + # Request image bytes + if kaleido_major() > 0: + # Kaleido v1 + # Check if trying to export to EPS format, which is not supported in Kaleido v1 + if format == "eps": + raise ValueError( + f""" +EPS export is not supported by Kaleido v1. Please use SVG or PDF instead. +You can also downgrade to Kaleido v0, but support for Kaleido v0 will be removed after {ENGINE_SUPPORT_TIMELINE}. +To downgrade to Kaleido v0, run: + $ pip install 'kaleido<1.0.0' +""" + ) + from kaleido.errors import ChromeNotFoundError + + try: + kopts = {} + if defaults.plotlyjs: + kopts["plotlyjs"] = defaults.plotlyjs + if defaults.mathjax: + kopts["mathjax"] = defaults.mathjax + + # TODO: Refactor to make it possible to use a shared Kaleido instance here + img_bytes = kaleido.calc_fig_sync( + fig_dict, + opts=dict( + format=format or defaults.default_format, + width=width or defaults.default_width, + height=height or defaults.default_height, + scale=scale or defaults.default_scale, + ), + topojson=defaults.topojson, + kopts=kopts, + ) + except ChromeNotFoundError: + raise RuntimeError(PLOTLY_GET_CHROME_ERROR_MSG) + + else: + # Kaleido v0 + if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: + warnings.warn(KALEIDO_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) + img_bytes = scope.transform( + fig_dict, format=format, width=width, height=height, scale=scale + ) + + return img_bytes + + +def write_image( + fig: Union[dict, plotly.graph_objects.Figure], + file: Union[str, Path], + format: Union[str, None] = None, + scale: Union[int, float, None] = None, + width: Union[int, None] = None, + height: Union[int, None] = None, + validate: bool = True, + # Deprecated + engine: Union[str, None] = "auto", +): + """ + Convert a figure to a static image and write it to a file or writeable + object + + 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. a pathlib.Path object or an open file descriptor) + + format: str or None + The desired image format. One of + - 'png' + - 'jpg' or 'jpeg' + - 'webp' + - 'svg' + - 'pdf' + - 'eps' (deprecated) (Requires the poppler library to be installed and on the PATH) + + If not specified and `file` is a string then this will default to the + file extension. If not specified and `file` is not a string then this + will default to: + - `plotly.io.defaults.default_format` if engine is "kaleido" + - `plotly.io.orca.config.default_format` if engine is "orca" (deprecated) + + width: int or None + The width of the exported image in layout pixels. If the `scale` + property is 1.0, this will also be the width of the exported image + in physical pixels. + + If not specified, will default to: + - `plotly.io.defaults.default_width` if engine is "kaleido" + - `plotly.io.orca.config.default_width` if engine is "orca" (deprecated) + + height: int or None + The height of the exported image in layout pixels. If the `scale` + property is 1.0, this will also be the height of the exported image + in physical pixels. + + If not specified, will default to: + - `plotly.io.defaults.default_height` if engine is "kaleido" + - `plotly.io.orca.config.default_height` if engine is "orca" (deprecated) + + scale: int or float or None + The scale factor to use when exporting the figure. A scale factor + larger than 1.0 will increase the image resolution with respect + to the figure's layout pixel dimensions. Whereas as scale factor of + less than 1.0 will decrease the image resolution. + + If not specified, will default to: + - `plotly.io.defaults.default_scale` if engine is "kaleido" + - `plotly.io.orca.config.default_scale` if engine is "orca" (deprecated) + + validate: bool + True if the figure should be validated before being converted to + an image, False otherwise. + + engine (deprecated): str + Image export engine to use. This parameter is deprecated and Orca engine support will be + dropped in the next major Plotly version. Until then, the following values are supported: + - "kaleido": Use Kaleido for image export + - "orca": Use Orca for image export + - "auto" (default): Use Kaleido if installed, otherwise use Orca + + Returns + ------- + None + """ + # Show Kaleido deprecation warning if needed + if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: + if ( + engine in {None, "auto", "kaleido"} + and kaleido_available() + and kaleido_major() < 1 + ): + warnings.warn(KALEIDO_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) + if engine == "orca": + warnings.warn(ORCA_DEPRECATION_MSG, DeprecationWarning, stacklevel=2) + if engine not in {None, "auto"}: + warnings.warn( + ENGINE_PARAM_DEPRECATION_MSG, DeprecationWarning, stacklevel=2 + ) + + # Try to cast `file` as a pathlib object `path`. + path = as_path_object(file) + + # Infer image format if not specified + format = infer_format(path, format) + + # Request image + # Do this first so we don't create a file if image conversion fails + img_data = to_image( + fig, + format=format, + scale=scale, + width=width, + height=height, + validate=validate, + engine=engine, + ) + + # Open file + if path is None: + # We previously failed to make sense of `file` as a pathlib object. + # Attempt to write to `file` as an open file descriptor. + try: + file.write(img_data) + return + except AttributeError: + pass + raise ValueError( + f""" +The 'file' argument '{file}' is not a string, pathlib.Path object, or file descriptor. +""" + ) + else: + # We previously succeeded in interpreting `file` as a pathlib object. + # Now we can use `write_bytes()`. + path.write_bytes(img_data) + + +def write_images( + fig: Union[ + List[Union[dict, plotly.graph_objects.Figure]], + Union[dict, plotly.graph_objects.Figure], + ], + file: Union[List[Union[str, Path]], Union[str, Path]], + format: Union[List[Union[str, None]], Union[str, None]] = None, + scale: Union[List[Union[int, float, None]], Union[int, float, None]] = None, + width: Union[List[Union[int, None]], Union[int, None]] = None, + height: Union[List[Union[int, None]], Union[int, None]] = None, + validate: Union[List[bool], bool] = True, +) -> None: + """ + Write multiple images to files or writeable objects. This is much faster than + calling write_image() multiple times. This function can only be used with the Kaleido + engine, v1.0.0 or greater. + + This function accepts the same arguments as write_image() (minus the `engine` argument), + except that any of the arguments may be either a single value or an iterable of values. + If multiple arguments are iterable, they must all have the same length. + + Parameters + ---------- + fig: + List of figure objects or dicts representing a figure. + Also accepts a single figure or dict representing a figure. + + file: str, pathlib.Path, or list of (str or pathlib.Path) + List of str or pathlib.Path objects representing local file paths to write to. + Can also be a single str or pathlib.Path object if fig argument is + a single figure or dict representing a figure. + + format: str, None, or list of (str or None) + The image format to use for exported images. + Supported formats are: + - 'png' + - 'jpg' or 'jpeg' + - 'webp' + - 'svg' + - 'pdf' + + Use a list to specify formats for each figure or dict in the list + provided to the `fig` argument. + Specify format as a `str` to apply the same format to all exported images. + If not specified, and the corresponding `file` argument has a file extension, then `format` will default to the + file extension. Otherwise, will default to `plotly.io.defaults.default_format`. + + width: int, None, or list of (int or None) + The width of the exported image in layout pixels. If the `scale` + property is 1.0, this will also be the width of the exported image + in physical pixels. + + Use a list to specify widths for each figure or dict in the list + provided to the `fig` argument. + Specify width as an `int` to apply the same width to all exported images. + If not specified, will default to `plotly.io.defaults.default_width`. + + height: int, None, or list of (int or None) + The height of the exported image in layout pixels. If the `scale` + property is 1.0, this will also be the height of the exported image + in physical pixels. + + Use a list to specify heights for each figure or dict in the list + provided to the `fig` argument. + Specify height as an `int` to apply the same height to all exported images. + If not specified, will default to `plotly.io.defaults.default_height`. + + scale: int, float, None, or list of (int, float, or None) + The scale factor to use when exporting the figure. A scale factor + larger than 1.0 will increase the image resolution with respect + to the figure's layout pixel dimensions. Whereas as scale factor of + less than 1.0 will decrease the image resolution. + + Use a list to specify scale for each figure or dict in the list + provided to the `fig` argument. + Specify scale as an `int` or `float` to apply the same scale to all exported images. + If not specified, will default to `plotly.io.defaults.default_scale`. + + validate: bool or list of bool + True if the figure should be validated before being converted to + an image, False otherwise. + + Use a list to specify validation setting for each figure in the list + provided to the `fig` argument. + Specify validate as a boolean to apply the same validation setting to all figures. + + Returns + ------- + None + """ + + # Raise informative error message if Kaleido v1 is not installed + if not kaleido_available(): + raise ValueError( + """ +The `write_images()` function requires the Kaleido package, +which can be installed using pip: + + $ pip install --upgrade kaleido +""" + ) + elif kaleido_major() < 1: + raise ValueError( + f""" +You have Kaleido version {Version(importlib_metadata.version("kaleido"))} installed. +The `write_images()` function requires the Kaleido package version 1.0.0 or greater, +which can be installed using pip: + + $ pip install 'kaleido>=1.0.0' +""" + ) + + # Broadcast arguments into correct format for passing to Kaleido + arg_dicts = broadcast_args_to_dicts( + fig=fig, + file=file, + format=format, + scale=scale, + width=width, + height=height, + validate=validate, + ) + + # For each dict: + # - convert figures to dicts (and validate if requested) + # - try to cast `file` as a Path object + for d in arg_dicts: + d["fig"] = validate_coerce_fig_to_dict(d["fig"], d["validate"]) + d["file"] = as_path_object(d["file"]) + + # Reshape arg_dicts into correct format for passing to Kaleido + # We call infer_format() here rather than above so that the `file` argument + # has already been cast to a Path object. + # Also insert defaults for any missing arguments as needed + kaleido_specs = [ + dict( + fig=d["fig"], + path=d["file"], + opts=dict( + format=infer_format(d["file"], d["format"]) or defaults.default_format, + width=d["width"] or defaults.default_width, + height=d["height"] or defaults.default_height, + scale=d["scale"] or defaults.default_scale, + ), + topojson=defaults.topojson, + ) + for d in arg_dicts + ] + + from kaleido.errors import ChromeNotFoundError + + try: + kopts = {} + if defaults.plotlyjs: + kopts["plotlyjs"] = defaults.plotlyjs + if defaults.mathjax: + kopts["mathjax"] = defaults.mathjax + kaleido.write_fig_from_object_sync( + kaleido_specs, + kopts=kopts, + ) + except ChromeNotFoundError: + raise RuntimeError(PLOTLY_GET_CHROME_ERROR_MSG) + + +def full_figure_for_development( + fig: Union[dict, plotly.graph_objects.Figure], + warn: bool = True, + as_dict: bool = False, +) -> Union[plotly.graph_objects.Figure, dict]: + """ + Compute default values for all attributes not specified in the input figure and + returns the output as a "full" figure. This function calls Plotly.js via Kaleido + to populate unspecified attributes. This function is intended for interactive use + during development to learn more about how Plotly.js computes default values and is + not generally necessary or recommended for production use. + + Parameters + ---------- + fig: + Figure object or dict representing a figure + + warn: bool + If False, suppress warnings about not using this in production. + + as_dict: bool + If True, output is a dict with some keys that go.Figure can't parse. + If False, output is a go.Figure with unparseable keys skipped. + + Returns + ------- + plotly.graph_objects.Figure or dict + The full figure + """ + + # Raise informative error message if Kaleido is not installed + if not kaleido_available(): + raise ValueError( + """ +Full figure generation requires the Kaleido package, +which can be installed using pip: + + $ pip install --upgrade kaleido +""" + ) + + if warn: + warnings.warn( + "full_figure_for_development is not recommended or necessary for " + "production use in most circumstances. \n" + "To suppress this warning, set warn=False" + ) + + if kaleido_available() and kaleido_major() > 0: + # Kaleido v1 + bytes = kaleido.calc_fig_sync( + fig, + opts=dict(format="json"), + ) + fig = json.loads(bytes.decode("utf-8")) + else: + # Kaleido v0 + if ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS: + warnings.warn( + f"Support for Kaleido versions less than 1.0.0 is deprecated and will be removed after {ENGINE_SUPPORT_TIMELINE}. " + + "Please upgrade Kaleido to version 1.0.0 or greater (`pip install 'kaleido>=1.0.0'`).", + DeprecationWarning, + ) + fig = json.loads(scope.transform(fig, format="json").decode("utf-8")) + + if as_dict: + return fig + else: + import plotly.graph_objects as go + + return go.Figure(fig, skip_invalid=True) + + +def get_chrome() -> None: + """ + Install Google Chrome for Kaleido (Required for Plotly image export). + This function can be run from the command line using the command `plotly_get_chrome` + defined in pyproject.toml + """ + + usage = """ +Usage: plotly_get_chrome [-y] [--path PATH] + +Installs Google Chrome for Plotly image export. + +Options: + -y Skip confirmation prompt + --path PATH Specify the path to install Chrome. Must be a path to an existing directory. + --help Show this message and exit. +""" + + if not kaleido_available() or kaleido_major() < 1: + raise ValueError( + """ +This command requires Kaleido v1.0.0 or greater. +Install it using `pip install 'kaleido>=1.0.0'` or `pip install 'plotly[kaleido]'`." +""" + ) + + # Handle command line arguments + import sys + + cli_args = sys.argv + + # Handle "-y" flag + cli_yes = "-y" in cli_args + if cli_yes: + cli_args.remove("-y") + + # Handle "--path" flag + chrome_install_path = None + user_specified_path = False + if "--path" in cli_args: + path_index = cli_args.index("--path") + 1 + if path_index < len(cli_args): + chrome_install_path = cli_args[path_index] + cli_args.remove("--path") + cli_args.remove(chrome_install_path) + chrome_install_path = Path(chrome_install_path) + user_specified_path = True + else: + from choreographer.cli.defaults import default_download_path + + chrome_install_path = default_download_path + + # If install path was chosen by user, make sure there is an existing directory + # located at chrome_install_path; otherwise fail + if user_specified_path: + if not chrome_install_path.exists(): + raise ValueError( + f""" +The specified install path '{chrome_install_path}' does not exist. +Please specify a path to an existing directory using the --path argument, +or omit the --path argument to use the default download path. +""" + ) + # Make sure the path is a directory + if not chrome_install_path.is_dir(): + raise ValueError( + f""" +The specified install path '{chrome_install_path}' already exists but is not a directory. +Please specify a path to an existing directory using the --path argument, +or omit the --path argument to use the default download path. +""" + ) + + # If any arguments remain, command syntax was incorrect -- print usage and exit + if len(cli_args) > 1: + print(usage) + sys.exit(1) + + if not cli_yes: + print( + f""" +Plotly will install a copy of Google Chrome to be used for generating static images of plots. +Chrome will be installed at: {chrome_install_path}""" + ) + response = input("Do you want to proceed? [y/n] ") + if not response or response[0].lower() != "y": + print("Cancelled") + return + print("Installing Chrome for Plotly...") + exe_path = kaleido.get_chrome_sync(path=chrome_install_path) + print("Chrome installed successfully.") + print(f"The Chrome executable is now located at: {exe_path}") + + +__all__ = ["to_image", "write_image", "scope", "full_figure_for_development"] diff --git a/venv/lib/python3.8/site-packages/plotly/io/_orca.py b/venv/lib/python3.8/site-packages/plotly/io/_orca.py new file mode 100644 index 0000000..2984210 --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/_orca.py @@ -0,0 +1,1670 @@ +import atexit +import functools +import json +import os +import random +import socket +import subprocess +import sys +import threading +import time +import warnings +from contextlib import contextmanager +from copy import copy +from pathlib import Path +from shutil import which + +import plotly +from plotly.files import PLOTLY_DIR, ensure_writable_plotly_dir +from plotly.io._utils import validate_coerce_fig_to_dict +from plotly.optional_imports import get_module + +psutil = get_module("psutil") + +# Valid image format constants +# ---------------------------- +valid_formats = ("png", "jpeg", "webp", "svg", "pdf", "eps") +format_conversions = {fmt: fmt for fmt in valid_formats} +format_conversions.update({"jpg": "jpeg"}) + + +# Utility functions +# ----------------- +def raise_format_value_error(val): + raise ValueError( + """ +Invalid value of type {typ} receive as an image format specification. + Received value: {v} + +An image format must be specified as one of the following string values: + {valid_formats}""".format( + typ=type(val), v=val, valid_formats=sorted(format_conversions.keys()) + ) + ) + + +def validate_coerce_format(fmt): + """ + Validate / coerce a user specified image format, and raise an informative + exception if format is invalid. + + Parameters + ---------- + fmt + A value that may or may not be a valid image format string. + + Returns + ------- + str or None + A valid image format string as supported by orca. This may not + be identical to the input image designation. For example, + the resulting string will always be lower case and 'jpg' is + converted to 'jpeg'. + + If the input format value is None, then no exception is raised and + None is returned. + + Raises + ------ + ValueError + if the input `fmt` cannot be interpreted as a valid image format. + """ + + # Let None pass through + if fmt is None: + return None + + # Check format type + if not isinstance(fmt, str) or not fmt: + raise_format_value_error(fmt) + + # Make lower case + fmt = fmt.lower() + + # Remove leading period, if any. + # For example '.png' is accepted and converted to 'png' + if fmt[0] == ".": + fmt = fmt[1:] + + # Check string value + if fmt not in format_conversions: + raise_format_value_error(fmt) + + # Return converted string specification + return format_conversions[fmt] + + +def find_open_port(): + """ + Use the socket module to find an open port. + + Returns + ------- + int + An open port + """ + s = socket.socket() + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind(("", 0)) + _, port = s.getsockname() + s.close() + + return port + + +def retry(min_wait=5, max_wait=10, max_delay=60000): + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + start_time = time.time() + + while True: + try: + return func(*args, **kwargs) + except Exception as e: + elapsed_time = time.time() - start_time + if elapsed_time * 1000 >= max_delay: + raise TimeoutError( + f"Retry limit of {max_delay} milliseconds reached." + ) from e + + wait_time = random.uniform(min_wait, max_wait) + print(f"Retrying in {wait_time:.2f} seconds due to {e}...") + time.sleep(wait_time) + + return wrapper + + return decorator + + +# Orca configuration class +# ------------------------ +class OrcaConfig(object): + """ + Singleton object containing the current user defined configuration + properties for orca. + + These parameters may optionally be saved to the user's ~/.plotly + directory using the `save` method, in which case they are automatically + restored in future sessions. + """ + + def __init__(self): + # Initialize properties dict + self._props = {} + + # Compute absolute path to the 'plotly/package_data/' directory + root_dir = os.path.dirname(os.path.abspath(plotly.__file__)) + self.package_dir = os.path.join(root_dir, "package_data") + + # Load pre-existing configuration + self.reload(warn=False) + + # Compute constants + plotlyjs = os.path.join(self.package_dir, "plotly.min.js") + self._constants = { + "plotlyjs": plotlyjs, + "config_file": os.path.join(PLOTLY_DIR, ".orca"), + } + + def restore_defaults(self, reset_server=True): + """ + Reset all orca configuration properties to their default values + """ + self._props = {} + + if reset_server: + # Server must restart before setting is active + reset_status() + + def update(self, d={}, **kwargs): + """ + Update one or more properties from a dict or from input keyword + arguments. + + Parameters + ---------- + d: dict + Dictionary from property names to new property values. + + kwargs + Named argument value pairs where the name is a configuration + property name and the value is the new property value. + + Returns + ------- + None + + Examples + -------- + Update configuration properties using a dictionary + + >>> import plotly.io as pio + >>> pio.orca.config.update({'timeout': 30, 'default_format': 'svg'}) + + Update configuration properties using keyword arguments + + >>> pio.orca.config.update(timeout=30, default_format='svg'}) + """ + # Combine d and kwargs + if not isinstance(d, dict): + raise ValueError( + """ +The first argument to update must be a dict, \ +but received value of type {typ}l + Received value: {val}""".format(typ=type(d), val=d) + ) + + updates = copy(d) + updates.update(kwargs) + + # Validate keys + for k in updates: + if k not in self._props: + raise ValueError("Invalid property name: {k}".format(k=k)) + + # Apply keys + for k, v in updates.items(): + setattr(self, k, v) + + def reload(self, warn=True): + """ + Reload orca settings from ~/.plotly/.orca, if any. + + Note: Settings are loaded automatically when plotly is imported. + This method is only needed if the setting are changed by some outside + process (e.g. a text editor) during an interactive session. + + Parameters + ---------- + warn: bool + If True, raise informative warnings if settings cannot be restored. + If False, do not raise warnings if setting cannot be restored. + + Returns + ------- + None + """ + if os.path.exists(self.config_file): + # ### Load file into a string ### + try: + with open(self.config_file, "r") as f: + orca_str = f.read() + except Exception: + if warn: + warnings.warn( + """\ +Unable to read orca configuration file at {path}""".format(path=self.config_file) + ) + return + + # ### Parse as JSON ### + try: + orca_props = json.loads(orca_str) + except ValueError: + if warn: + warnings.warn( + """\ +Orca configuration file at {path} is not valid JSON""".format(path=self.config_file) + ) + return + + # ### Update _props ### + for k, v in orca_props.items(): + self._props[k] = v + + elif warn: + warnings.warn( + """\ +Orca configuration file at {path} not found""".format(path=self.config_file) + ) + + def save(self): + """ + Attempt to save current settings to disk, so that they are + automatically restored for future sessions. + + This operation requires write access to the path returned by + in the `config_file` property. + + Returns + ------- + None + """ + if ensure_writable_plotly_dir(): + with open(self.config_file, "w") as f: + json.dump(self._props, f, indent=4) + else: + warnings.warn( + """\ +Failed to write orca configuration file at '{path}'""".format(path=self.config_file) + ) + + @property + def server_url(self): + """ + The server URL to use for an external orca server, or None if orca + should be managed locally + + Overrides executable, port, timeout, mathjax, topojson, + and mapbox_access_token + + Returns + ------- + str or None + """ + return self._props.get("server_url", None) + + @server_url.setter + def server_url(self, val): + if val is None: + self._props.pop("server_url", None) + return + if not isinstance(val, str): + raise ValueError( + """ +The server_url property must be a string, but received value of type {typ}. + Received value: {val}""".format(typ=type(val), val=val) + ) + + if not val.startswith("http://") and not val.startswith("https://"): + val = "http://" + val + + shutdown_server() + self.executable = None + self.port = None + self.timeout = None + self.mathjax = None + self.topojson = None + self.mapbox_access_token = None + self._props["server_url"] = val + + @property + def port(self): + """ + The specific port to use to communicate with the orca server, or + None if the port is to be chosen automatically. + + If an orca server is active, the port in use is stored in the + plotly.io.orca.status.port property. + + Returns + ------- + int or None + """ + return self._props.get("port", None) + + @port.setter + def port(self, val): + if val is None: + self._props.pop("port", None) + return + if not isinstance(val, int): + raise ValueError( + """ +The port property must be an integer, but received value of type {typ}. + Received value: {val}""".format(typ=type(val), val=val) + ) + + self._props["port"] = val + + @property + def executable(self): + """ + The name or full path of the orca executable. + + - If a name (e.g. 'orca'), then it should be the name of an orca + executable on the PATH. The directories on the PATH can be + displayed by running the following command: + + >>> import os + >>> print(os.environ.get('PATH').replace(os.pathsep, os.linesep)) + + - If a full path (e.g. '/path/to/orca'), then + it should be the full path to an orca executable. In this case + the executable does not need to reside on the PATH. + + If an orca server has been validated, then the full path to the + validated orca executable is stored in the + plotly.io.orca.status.executable property. + + Returns + ------- + str + """ + executable_list = self._props.get("executable_list", ["orca"]) + if executable_list is None: + return None + else: + return " ".join(executable_list) + + @executable.setter + def executable(self, val): + if val is None: + self._props.pop("executable", None) + else: + if not isinstance(val, str): + raise ValueError( + """ +The executable property must be a string, but received value of type {typ}. + Received value: {val}""".format(typ=type(val), val=val) + ) + if isinstance(val, str): + val = [val] + self._props["executable_list"] = val + + # Server and validation must restart before setting is active + reset_status() + + @property + def timeout(self): + """ + The number of seconds of inactivity required before the orca server + is shut down. + + For example, if timeout is set to 20, then the orca + server will shutdown once is has not been used for at least + 20 seconds. If timeout is set to None, then the server will not be + automatically shut down due to inactivity. + + Regardless of the value of timeout, a running orca server may be + manually shut down like this: + + >>> import plotly.io as pio + >>> pio.orca.shutdown_server() + + Returns + ------- + int or float or None + """ + return self._props.get("timeout", None) + + @timeout.setter + def timeout(self, val): + if val is None: + self._props.pop("timeout", None) + else: + if not isinstance(val, (int, float)): + raise ValueError( + """ +The timeout property must be a number, but received value of type {typ}. + Received value: {val}""".format(typ=type(val), val=val) + ) + self._props["timeout"] = val + + # Server must restart before setting is active + shutdown_server() + + @property + def default_width(self): + """ + The default width to use on image export. This value is only + applied if no width value is supplied to the plotly.io + to_image or write_image functions. + + Returns + ------- + int or None + """ + return self._props.get("default_width", None) + + @default_width.setter + def default_width(self, val): + if val is None: + self._props.pop("default_width", None) + return + if not isinstance(val, int): + raise ValueError( + """ +The default_width property must be an int, but received value of type {typ}. + Received value: {val}""".format(typ=type(val), val=val) + ) + self._props["default_width"] = val + + @property + def default_height(self): + """ + The default height to use on image export. This value is only + applied if no height value is supplied to the plotly.io + to_image or write_image functions. + + Returns + ------- + int or None + """ + return self._props.get("default_height", None) + + @default_height.setter + def default_height(self, val): + if val is None: + self._props.pop("default_height", None) + return + if not isinstance(val, int): + raise ValueError( + """ +The default_height property must be an int, but received value of type {typ}. + Received value: {val}""".format(typ=type(val), val=val) + ) + self._props["default_height"] = val + + @property + def default_format(self): + """ + The default image format to use on image export. + + Valid image formats strings are: + - 'png' + - 'jpg' or 'jpeg' + - 'webp' + - 'svg' + - 'pdf' + - 'eps' (Requires the poppler library to be installed) + + This value is only applied if no format value is supplied to the + plotly.io to_image or write_image functions. + + Returns + ------- + str or None + """ + return self._props.get("default_format", "png") + + @default_format.setter + def default_format(self, val): + if val is None: + self._props.pop("default_format", None) + return + + val = validate_coerce_format(val) + self._props["default_format"] = val + + @property + def default_scale(self): + """ + The default image scaling factor to use on image export. + This value is only applied if no scale value is supplied to the + plotly.io to_image or write_image functions. + + Returns + ------- + int or None + """ + return self._props.get("default_scale", 1) + + @default_scale.setter + def default_scale(self, val): + if val is None: + self._props.pop("default_scale", None) + return + if not isinstance(val, (int, float)): + raise ValueError( + """ +The default_scale property must be a number, but received value of type {typ}. + Received value: {val}""".format(typ=type(val), val=val) + ) + self._props["default_scale"] = val + + @property + def topojson(self): + """ + Path to the topojson files needed to render choropleth traces. + + If None, topojson files from the plot.ly CDN are used. + + Returns + ------- + str + """ + return self._props.get("topojson", None) + + @topojson.setter + def topojson(self, val): + if val is None: + self._props.pop("topojson", None) + else: + if not isinstance(val, str): + raise ValueError( + """ +The topojson property must be a string, but received value of type {typ}. + Received value: {val}""".format(typ=type(val), val=val) + ) + self._props["topojson"] = val + + # Server must restart before setting is active + shutdown_server() + + @property + def mathjax(self): + """ + Path to the MathJax bundle needed to render LaTeX characters + + Returns + ------- + str + """ + return self._props.get( + "mathjax", + "https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js", + ) + + @mathjax.setter + def mathjax(self, val): + if val is None: + self._props.pop("mathjax", None) + else: + if not isinstance(val, str): + raise ValueError( + """ +The mathjax property must be a string, but received value of type {typ}. + Received value: {val}""".format(typ=type(val), val=val) + ) + self._props["mathjax"] = val + + # Server must restart before setting is active + shutdown_server() + + @property + def mapbox_access_token(self): + """ + Mapbox access token required to render mapbox traces. + + Returns + ------- + str + """ + return self._props.get("mapbox_access_token", None) + + @mapbox_access_token.setter + def mapbox_access_token(self, val): + if val is None: + self._props.pop("mapbox_access_token", None) + else: + if not isinstance(val, str): + raise ValueError( + """ +The mapbox_access_token property must be a string, \ +but received value of type {typ}. + Received value: {val}""".format(typ=type(val), val=val) + ) + self._props["mapbox_access_token"] = val + + # Server must restart before setting is active + shutdown_server() + + @property + def use_xvfb(self): + dflt = "auto" + return self._props.get("use_xvfb", dflt) + + @use_xvfb.setter + def use_xvfb(self, val): + valid_vals = [True, False, "auto"] + if val is None: + self._props.pop("use_xvfb", None) + else: + if val not in valid_vals: + raise ValueError( + """ +The use_xvfb property must be one of {valid_vals} + Received value of type {typ}: {val}""".format( + valid_vals=valid_vals, typ=type(val), val=repr(val) + ) + ) + + self._props["use_xvfb"] = val + + # Server and validation must restart before setting is active + reset_status() + + @property + def plotlyjs(self): + """ + The plotly.js bundle being used for image rendering. + + Returns + ------- + str + """ + return self._constants.get("plotlyjs", None) + + @property + def config_file(self): + """ + Path to orca configuration file + + Using the `plotly.io.config.save()` method will save the current + configuration settings to this file. Settings in this file are + restored at the beginning of each sessions. + + Returns + ------- + str + """ + return os.path.join(PLOTLY_DIR, ".orca") + + def __repr__(self): + """ + Display a nice representation of the current orca configuration. + """ + return """\ +orca configuration +------------------ + server_url: {server_url} + executable: {executable} + port: {port} + timeout: {timeout} + default_width: {default_width} + default_height: {default_height} + default_scale: {default_scale} + default_format: {default_format} + mathjax: {mathjax} + topojson: {topojson} + mapbox_access_token: {mapbox_access_token} + use_xvfb: {use_xvfb} + +constants +--------- + plotlyjs: {plotlyjs} + config_file: {config_file} + +""".format( + server_url=self.server_url, + port=self.port, + executable=self.executable, + timeout=self.timeout, + default_width=self.default_width, + default_height=self.default_height, + default_scale=self.default_scale, + default_format=self.default_format, + mathjax=self.mathjax, + topojson=self.topojson, + mapbox_access_token=self.mapbox_access_token, + plotlyjs=self.plotlyjs, + config_file=self.config_file, + use_xvfb=self.use_xvfb, + ) + + +# Make config a singleton object +# ------------------------------ +config = OrcaConfig() +del OrcaConfig + + +# Orca status class +# ------------------------ +class OrcaStatus(object): + """ + Class to store information about the current status of the orca server. + """ + + _props = { + "state": "unvalidated", # or 'validated' or 'running' + "executable_list": None, + "version": None, + "pid": None, + "port": None, + "command": None, + } + + @property + def state(self): + """ + A string representing the state of the orca server process + + One of: + - unvalidated: The orca executable has not yet been searched for or + tested to make sure its valid. + - validated: The orca executable has been located and tested for + validity, but it is not running. + - running: The orca server process is currently running. + """ + return self._props["state"] + + @property + def executable(self): + """ + If the `state` property is 'validated' or 'running', this property + contains the full path to the orca executable. + + This path can be specified explicitly by setting the `executable` + property of the `plotly.io.orca.config` object. + + This property will be None if the `state` is 'unvalidated'. + """ + executable_list = self._props["executable_list"] + if executable_list is None: + return None + else: + return " ".join(executable_list) + + @property + def version(self): + """ + If the `state` property is 'validated' or 'running', this property + contains the version of the validated orca executable. + + This property will be None if the `state` is 'unvalidated'. + """ + return self._props["version"] + + @property + def pid(self): + """ + The process id of the orca server process, if any. This property + will be None if the `state` is not 'running'. + """ + return self._props["pid"] + + @property + def port(self): + """ + The port number that the orca server process is listening to, if any. + This property will be None if the `state` is not 'running'. + + This port can be specified explicitly by setting the `port` + property of the `plotly.io.orca.config` object. + """ + return self._props["port"] + + @property + def command(self): + """ + The command arguments used to launch the running orca server, if any. + This property will be None if the `state` is not 'running'. + """ + return self._props["command"] + + def __repr__(self): + """ + Display a nice representation of the current orca server status. + """ + return """\ +orca status +----------- + state: {state} + executable: {executable} + version: {version} + port: {port} + pid: {pid} + command: {command} + +""".format( + executable=self.executable, + version=self.version, + port=self.port, + pid=self.pid, + state=self.state, + command=self.command, + ) + + +# Make status a singleton object +# ------------------------------ +status = OrcaStatus() +del OrcaStatus + + +@contextmanager +def orca_env(): + """ + Context manager to clear and restore environment variables that are + problematic for orca to function properly + + NODE_OPTIONS: When this variable is set, orca <v1.2 will have a + segmentation fault due to an electron bug. + See: https://github.com/electron/electron/issues/12695 + + ELECTRON_RUN_AS_NODE: When this environment variable is set the call + to orca is transformed into a call to nodejs. + See https://github.com/plotly/orca/issues/149#issuecomment-443506732 + """ + clear_env_vars = ["NODE_OPTIONS", "ELECTRON_RUN_AS_NODE", "LD_PRELOAD"] + orig_env_vars = {} + + try: + # Clear and save + orig_env_vars.update( + {var: os.environ.pop(var) for var in clear_env_vars if var in os.environ} + ) + yield + finally: + # Restore + for var, val in orig_env_vars.items(): + os.environ[var] = val + + +# Public orca server interaction functions +# ---------------------------------------- +def validate_executable(): + """ + Attempt to find and validate the orca executable specified by the + `plotly.io.orca.config.executable` property. + + If the `plotly.io.orca.status.state` property is 'validated' or 'running' + then this function does nothing. + + How it works: + - First, it searches the system PATH for an executable that matches the + name or path specified in the `plotly.io.orca.config.executable` + property. + - Then it runs the executable with the `--help` flag to make sure + it's the plotly orca executable + - Then it runs the executable with the `--version` flag to check the + orca version. + + If all of these steps are successful then the `status.state` property + is set to 'validated' and the `status.executable` and `status.version` + properties are populated + + Returns + ------- + None + """ + # Check state + # ----------- + if status.state != "unvalidated": + # Nothing more to do + return + + # Initialize error messages + # ------------------------- + install_location_instructions = """\ +If you haven't installed orca yet, you can do so using conda as follows: + + $ conda install -c plotly plotly-orca + +Alternatively, see other installation methods in the orca project README at +https://github.com/plotly/orca + +After installation is complete, no further configuration should be needed. + +If you have installed orca, then for some reason plotly.py was unable to +locate it. In this case, set the `plotly.io.orca.config.executable` +property to the full path of your orca executable. For example: + + >>> plotly.io.orca.config.executable = '/path/to/orca' + +After updating this executable property, try the export operation again. +If it is successful then you may want to save this configuration so that it +will be applied automatically in future sessions. You can do this as follows: + + >>> plotly.io.orca.config.save() + +If you're still having trouble, feel free to ask for help on the forums at +https://community.plot.ly/c/api/python +""" + + # Try to find an executable + # ------------------------- + # Search for executable name or path in config.executable + executable = which(config.executable) + path = os.environ.get("PATH", os.defpath) + formatted_path = path.replace(os.pathsep, "\n ") + + if executable is None: + raise ValueError( + """ +The orca executable is required to export figures as static images, +but it could not be found on the system path. + +Searched for executable '{executable}' on the following path: + {formatted_path} + +{instructions}""".format( + executable=config.executable, + formatted_path=formatted_path, + instructions=install_location_instructions, + ) + ) + + # Check if we should run with Xvfb + # -------------------------------- + xvfb_args = [ + "--auto-servernum", + "--server-args", + "-screen 0 640x480x24 +extension RANDR +extension GLX", + executable, + ] + + if config.use_xvfb: + # Use xvfb + xvfb_run_executable = which("xvfb-run") + if not xvfb_run_executable: + raise ValueError( + """ +The plotly.io.orca.config.use_xvfb property is set to True, but the +xvfb-run executable could not be found on the system path. + +Searched for the executable 'xvfb-run' on the following path: + {formatted_path}""".format(formatted_path=formatted_path) + ) + + executable_list = [xvfb_run_executable] + xvfb_args + elif ( + config.use_xvfb == "auto" + and sys.platform.startswith("linux") + and not os.environ.get("DISPLAY") + and which("xvfb-run") + ): + # use_xvfb is 'auto', we're on linux without a display server, + # and xvfb-run is available. Use it. + xvfb_run_executable = which("xvfb-run") + executable_list = [xvfb_run_executable] + xvfb_args + else: + # Do not use xvfb + executable_list = [executable] + + # Run executable with --help and see if it's our orca + # --------------------------------------------------- + invalid_executable_msg = """ +The orca executable is required in order to export figures as static images, +but the executable that was found at '{executable}' +does not seem to be a valid plotly orca executable. Please refer to the end of +this message for details on what went wrong. + +{instructions}""".format( + executable=executable, instructions=install_location_instructions + ) + + # ### Run with Popen so we get access to stdout and stderr + with orca_env(): + p = subprocess.Popen( + executable_list + ["--help"], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + help_result, help_error = p.communicate() + + if p.returncode != 0: + err_msg = ( + invalid_executable_msg + + """ +Here is the error that was returned by the command + $ {executable} --help + +[Return code: {returncode}] +{err_msg} +""".format( + executable=" ".join(executable_list), + err_msg=help_error.decode("utf-8"), + returncode=p.returncode, + ) + ) + + # Check for Linux without X installed. + if sys.platform.startswith("linux") and not os.environ.get("DISPLAY"): + err_msg += """\ +Note: When used on Linux, orca requires an X11 display server, but none was +detected. Please install Xvfb and configure plotly.py to run orca using Xvfb +as follows: + + >>> import plotly.io as pio + >>> pio.orca.config.use_xvfb = True + +You can save this configuration for use in future sessions as follows: + + >>> pio.orca.config.save() + +See https://www.x.org/releases/X11R7.6/doc/man/man1/Xvfb.1.xhtml +for more info on Xvfb +""" + raise ValueError(err_msg) + + if not help_result: + raise ValueError( + invalid_executable_msg + + """ +The error encountered is that no output was returned by the command + $ {executable} --help +""".format(executable=" ".join(executable_list)) + ) + + if "Plotly's image-exporting utilities" not in help_result.decode("utf-8"): + raise ValueError( + invalid_executable_msg + + """ +The error encountered is that unexpected output was returned by the command + $ {executable} --help + +{help_result} +""".format(executable=" ".join(executable_list), help_result=help_result) + ) + + # Get orca version + # ---------------- + # ### Run with Popen so we get access to stdout and stderr + with orca_env(): + p = subprocess.Popen( + executable_list + ["--version"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + version_result, version_error = p.communicate() + + if p.returncode != 0: + raise ValueError( + invalid_executable_msg + + """ +An error occurred while trying to get the version of the orca executable. +Here is the command that plotly.py ran to request the version + $ {executable} --version + +This command returned the following error: + +[Return code: {returncode}] +{err_msg} + """.format( + executable=" ".join(executable_list), + err_msg=version_error.decode("utf-8"), + returncode=p.returncode, + ) + ) + + if not version_result: + raise ValueError( + invalid_executable_msg + + """ +The error encountered is that no version was reported by the orca executable. +Here is the command that plotly.py ran to request the version: + + $ {executable} --version +""".format(executable=" ".join(executable_list)) + ) + else: + version_result = version_result.decode() + + status._props["executable_list"] = executable_list + status._props["version"] = version_result.strip() + status._props["state"] = "validated" + + +def reset_status(): + """ + Shutdown the running orca server, if any, and reset the orca status + to unvalidated. + + This command is only needed if the desired orca executable is changed + during an interactive session. + + Returns + ------- + None + """ + shutdown_server() + status._props["executable_list"] = None + status._props["version"] = None + status._props["state"] = "unvalidated" + + +# Initialze process control variables +# ----------------------------------- +orca_lock = threading.Lock() +orca_state = {"proc": None, "shutdown_timer": None} + + +# Shutdown +# -------- +# The @atexit.register annotation ensures that the shutdown function is +# is run when the Python process is terminated +@atexit.register +def cleanup(): + shutdown_server() + + +def shutdown_server(): + """ + Shutdown the running orca server process, if any + + Returns + ------- + None + """ + # Use double-check locking to make sure the properties of orca_state + # are updated consistently across threads. + if orca_state["proc"] is not None: + with orca_lock: + if orca_state["proc"] is not None: + # We use psutil to kill all child processes of the main orca + # process. This prevents any zombie processes from being + # left over, and it saves us from needing to write + # OS-specific process management code here. + + parent = psutil.Process(orca_state["proc"].pid) + for child in parent.children(recursive=True): + try: + child.terminate() + except Exception: + # We tried, move on + pass + + try: + # Kill parent process + orca_state["proc"].terminate() + + # Wait for the process to shutdown + orca_state["proc"].wait() + except Exception: + # We tried, move on + pass + + # Update our internal process management state + orca_state["proc"] = None + + if orca_state["shutdown_timer"] is not None: + orca_state["shutdown_timer"].cancel() + orca_state["shutdown_timer"] = None + + orca_state["port"] = None + + # Update orca.status so the user has an accurate view + # of the state of the orca server + status._props["state"] = "validated" + status._props["pid"] = None + status._props["port"] = None + status._props["command"] = None + + +# Launch or get server +def ensure_server(): + """ + Start an orca server if none is running. If a server is already running, + then reset the timeout countdown + + Returns + ------- + None + """ + + # Validate psutil + if psutil is None: + raise ValueError( + """\ +Image generation requires the psutil package. + +Install using pip: + $ pip install psutil + +Install using conda: + $ conda install psutil +""" + ) + + # Validate requests + if not get_module("requests"): + raise ValueError( + """\ +Image generation requires the requests package. + +Install using pip: + $ pip install requests + +Install using conda: + $ conda install requests +""" + ) + + if not config.server_url: + # Validate orca executable only if server_url is not provided + if status.state == "unvalidated": + validate_executable() + # Acquire lock to make sure that we keep the properties of orca_state + # consistent across threads + with orca_lock: + # Cancel the current shutdown timer, if any + if orca_state["shutdown_timer"] is not None: + orca_state["shutdown_timer"].cancel() + + # Start a new server process if none is active + if orca_state["proc"] is None: + # Determine server port + if config.port is None: + orca_state["port"] = find_open_port() + else: + orca_state["port"] = config.port + + # Build orca command list + cmd_list = status._props["executable_list"] + [ + "serve", + "-p", + str(orca_state["port"]), + "--plotly", + config.plotlyjs, + "--graph-only", + ] + + if config.topojson: + cmd_list.extend(["--topojson", config.topojson]) + + if config.mathjax: + cmd_list.extend(["--mathjax", config.mathjax]) + + if config.mapbox_access_token: + cmd_list.extend( + ["--mapbox-access-token", config.mapbox_access_token] + ) + + # Create subprocess that launches the orca server on the + # specified port. + DEVNULL = open(os.devnull, "wb") + with orca_env(): + stderr = DEVNULL if "CI" in os.environ else None # fix for CI + orca_state["proc"] = subprocess.Popen( + cmd_list, stdout=DEVNULL, stderr=stderr + ) + + # Update orca.status so the user has an accurate view + # of the state of the orca server + status._props["state"] = "running" + status._props["pid"] = orca_state["proc"].pid + status._props["port"] = orca_state["port"] + status._props["command"] = cmd_list + + # Create new shutdown timer if a timeout was specified + if config.timeout is not None: + t = threading.Timer(config.timeout, shutdown_server) + # Make it a daemon thread so that exit won't wait for timer to + # complete + t.daemon = True + t.start() + orca_state["shutdown_timer"] = t + + +@retry(min_wait=5, max_wait=10, max_delay=60000) +def request_image_with_retrying(**kwargs): + """ + Helper method to perform an image request to a running orca server process + with retrying logic. + """ + from requests import post + from plotly.io.json import to_json_plotly + + if config.server_url: + server_url = config.server_url + else: + server_url = "http://{hostname}:{port}".format( + hostname="localhost", port=orca_state["port"] + ) + + request_params = {k: v for k, v in kwargs.items() if v is not None} + json_str = to_json_plotly(request_params) + response = post(server_url + "/", data=json_str) + + if response.status_code == 522: + # On "522: client socket timeout", return server and keep trying + shutdown_server() + ensure_server() + raise OSError("522: client socket timeout") + + return response + + +def to_image(fig, format=None, width=None, height=None, scale=None, validate=True): + """ + Convert a figure to a static image bytes string + + Parameters + ---------- + fig: + Figure object or dict representing a figure + + format: str or None + The desired image format. One of + - 'png' + - 'jpg' or 'jpeg' + - 'webp' + - 'svg' + - 'pdf' + - 'eps' (Requires the poppler library to be installed) + + If not specified, will default to `plotly.io.config.default_format` + + width: int or None + The width of the exported image in layout pixels. If the `scale` + property is 1.0, this will also be the width of the exported image + in physical pixels. + + If not specified, will default to `plotly.io.config.default_width` + + height: int or None + The height of the exported image in layout pixels. If the `scale` + property is 1.0, this will also be the height of the exported image + in physical pixels. + + If not specified, will default to `plotly.io.config.default_height` + + scale: int or float or None + The scale factor to use when exporting the figure. A scale factor + larger than 1.0 will increase the image resolution with respect + to the figure's layout pixel dimensions. Whereas as scale factor of + less than 1.0 will decrease the image resolution. + + If not specified, will default to `plotly.io.config.default_scale` + + validate: bool + True if the figure should be validated before being converted to + an image, False otherwise. + + Returns + ------- + bytes + The image data + """ + # Make sure orca sever is running + # ------------------------------- + ensure_server() + + # Handle defaults + # --------------- + # Apply configuration defaults to unspecified arguments + if format is None: + format = config.default_format + + format = validate_coerce_format(format) + + if scale is None: + scale = config.default_scale + + if width is None: + width = config.default_width + + if height is None: + height = config.default_height + + # Validate figure + # --------------- + fig_dict = validate_coerce_fig_to_dict(fig, validate) + + # Request image from server + # ------------------------- + try: + response = request_image_with_retrying( + figure=fig_dict, format=format, scale=scale, width=width, height=height + ) + except OSError: + # Get current status string + status_str = repr(status) + + if config.server_url: + raise ValueError( + """ +Plotly.py was unable to communicate with the orca server at {server_url} + +Please check that the server is running and accessible. +""".format(server_url=config.server_url) + ) + + else: + # Check if the orca server process exists + pid_exists = psutil.pid_exists(status.pid) + + # Raise error message based on whether the server process existed + if pid_exists: + raise ValueError( + """ +For some reason plotly.py was unable to communicate with the +local orca server process, even though the server process seems to be running. + +Please review the process and connection information below: + +{info} +""".format(info=status_str) + ) + else: + # Reset the status so that if the user tries again, we'll try to + # start the server again + reset_status() + raise ValueError( + """ +For some reason the orca server process is no longer running. + +Please review the process and connection information below: + +{info} +plotly.py will attempt to start the local server process again the next time +an image export operation is performed. +""".format(info=status_str) + ) + + # Check response + # -------------- + if response.status_code == 200: + # All good + return response.content + else: + # ### Something went wrong ### + err_message = """ +The image request was rejected by the orca conversion utility +with the following error: + {status}: {msg} +""".format(status=response.status_code, msg=response.content.decode("utf-8")) + + # ### Try to be helpful ### + # Status codes from /src/component/plotly-graph/constants.js in the + # orca code base. + # statusMsg: { + # 400: 'invalid or malformed request syntax', + # 522: client socket timeout + # 525: 'plotly.js error', + # 526: 'plotly.js version 1.11.0 or up required', + # 530: 'image conversion error' + # } + if response.status_code == 400 and isinstance(fig, dict) and not validate: + err_message += """ +Try setting the `validate` argument to True to check for errors in the +figure specification""" + elif response.status_code == 525: + any_mapbox = any( + [ + trace.get("type", None) == "scattermapbox" + for trace in fig_dict.get("data", []) + ] + ) + if any_mapbox and config.mapbox_access_token is None: + err_message += """ +Exporting scattermapbox traces requires a mapbox access token. +Create a token in your mapbox account and then set it using: + +>>> plotly.io.orca.config.mapbox_access_token = 'pk.abc...' + +If you would like this token to be applied automatically in +future sessions, then save your orca configuration as follows: + +>>> plotly.io.orca.config.save() +""" + elif response.status_code == 530 and format == "eps": + err_message += """ +Exporting to EPS format requires the poppler library. You can install +poppler on MacOS or Linux with: + + $ conda install poppler + +Or, you can install it on MacOS using homebrew with: + + $ brew install poppler + +Or, you can install it on Linux using your distribution's package manager to +install the 'poppler-utils' package. + +Unfortunately, we don't yet know of an easy way to install poppler on Windows. +""" + raise ValueError(err_message) + + +def write_image( + fig, file, format=None, scale=None, width=None, height=None, validate=True +): + """ + Convert a figure to a static image and write it to a file or writeable + object + + 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. a pathlib.Path object or an open file descriptor) + + format: str or None + The desired image format. One of + - 'png' + - 'jpg' or 'jpeg' + - 'webp' + - 'svg' + - 'pdf' + - 'eps' (Requires the poppler library to be installed) + + If not specified and `file` is a string then this will default to the + file extension. If not specified and `file` is not a string then this + will default to `plotly.io.config.default_format` + + width: int or None + The width of the exported image in layout pixels. If the `scale` + property is 1.0, this will also be the width of the exported image + in physical pixels. + + If not specified, will default to `plotly.io.config.default_width` + + height: int or None + The height of the exported image in layout pixels. If the `scale` + property is 1.0, this will also be the height of the exported image + in physical pixels. + + If not specified, will default to `plotly.io.config.default_height` + + scale: int or float or None + The scale factor to use when exporting the figure. A scale factor + larger than 1.0 will increase the image resolution with respect + to the figure's layout pixel dimensions. Whereas as scale factor of + less than 1.0 will decrease the image resolution. + + If not specified, will default to `plotly.io.config.default_scale` + + validate: bool + True if the figure should be validated before being converted to + an image, False otherwise. + + Returns + ------- + None + """ + + # Try to cast `file` as a pathlib object `path`. + # ---------------------------------------------- + if isinstance(file, str): + # Use the standard Path constructor to make a pathlib object. + path = Path(file) + elif isinstance(file, Path): + # `file` is already a Path object. + path = file + else: + # We could not make a Path object out of file. Either `file` is an open file + # descriptor with a `write()` method or it's an invalid object. + path = None + + # Infer format if not specified + # ----------------------------- + if path is not None and format is None: + ext = path.suffix + if ext: + format = ext.lstrip(".") + else: + raise ValueError( + """ +Cannot infer image type from output path '{file}'. +Please add a file extension or specify the type using the format parameter. +For example: + + >>> import plotly.io as pio + >>> pio.write_image(fig, file_path, format='png') +""".format(file=file) + ) + + # Request image + # ------------- + # Do this first so we don't create a file if image conversion fails + img_data = to_image( + fig, format=format, scale=scale, width=width, height=height, validate=validate + ) + + # Open file + # --------- + if path is None: + # We previously failed to make sense of `file` as a pathlib object. + # Attempt to write to `file` as an open file descriptor. + try: + file.write(img_data) + return + except AttributeError: + pass + raise ValueError( + """ +The 'file' argument '{file}' is not a string, pathlib.Path object, or file descriptor. +""".format(file=file) + ) + else: + # We previously succeeded in interpreting `file` as a pathlib object. + # Now we can use `write_bytes()`. + path.write_bytes(img_data) diff --git a/venv/lib/python3.8/site-packages/plotly/io/_renderers.py b/venv/lib/python3.8/site-packages/plotly/io/_renderers.py new file mode 100644 index 0000000..9ddd1db --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/_renderers.py @@ -0,0 +1,567 @@ +import textwrap +from copy import copy +import os +from packaging.version import Version +import warnings + +from plotly import optional_imports + +from plotly.io._base_renderers import ( + MimetypeRenderer, + ExternalRenderer, + PlotlyRenderer, + NotebookRenderer, + KaggleRenderer, + AzureRenderer, + ColabRenderer, + JsonRenderer, + PngRenderer, + JpegRenderer, + SvgRenderer, + PdfRenderer, + BrowserRenderer, + IFrameRenderer, + SphinxGalleryHtmlRenderer, + SphinxGalleryOrcaRenderer, + CoCalcRenderer, + DatabricksRenderer, +) +from plotly.io._utils import validate_coerce_fig_to_dict + +ipython = optional_imports.get_module("IPython") +ipython_display = optional_imports.get_module("IPython.display") +nbformat = optional_imports.get_module("nbformat") + + +def display_jupyter_version_warnings(): + parent_process = None + try: + psutil = optional_imports.get_module("psutil") + if psutil is not None: + parent_process = psutil.Process().parent().cmdline()[-1] + except Exception: + pass + + if parent_process is None: + return + elif "jupyter-notebook" in parent_process: + jupyter_notebook = optional_imports.get_module("notebook") + if jupyter_notebook is not None and jupyter_notebook.__version__ < "7": + # Add warning about upgrading notebook + warnings.warn( + f"Plotly version >= 6 requires Jupyter Notebook >= 7 but you have {jupyter_notebook.__version__} installed.\n To upgrade Jupyter Notebook, please run `pip install notebook --upgrade`." + ) + elif "jupyter-lab" in parent_process: + jupyter_lab = optional_imports.get_module("jupyterlab") + if jupyter_lab is not None and jupyter_lab.__version__ < "3": + # Add warning about upgrading jupyterlab + warnings.warn( + f"Plotly version >= 6 requires JupyterLab >= 3 but you have {jupyter_lab.__version__} installed. To upgrade JupyterLab, please run `pip install jupyterlab --upgrade`." + ) + + +# 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 = [] + self._render_on_display = False + self._to_activate = [] + + # ### 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, ExternalRenderer)): + raise ValueError( + """\ +Renderer must be a subclass of MimetypeRenderer or ExternalRenderer. + Received value with type: {typ}""".format(typ=type(value)) + ) + + 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 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 + the classic Jupyter Notebook, JupyterLab, and PDF export: + + >>> import plotly.io as pio + >>> pio.renderers.default = 'notebook+jupyterlab+pdf' + + The names of available renderers 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 not value: + # _default_name should always be a string so we can do + # pio.renderers.default.split('+') + self._default_name = "" + 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] + + # Register renderers for activation before their next use + self._to_activate = list(self._default_renderers) + + @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): + self._render_on_display = bool(val) + + 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 + 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, str): + 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_renderers_str()) + + def _available_renderers_str(self): + """ + Return nicely wrapped string representation of all + available renderer 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, **kwargs): + """ + 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] + + # 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 = {} + 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_external_rendering(self, fig_dict, renderers_string=None, **kwargs): + """ + 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 ExternalRenderer. + + 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] + + # 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: + if isinstance(renderer, ExternalRenderer): + renderer = copy(renderer) + for k, v in kwargs.items(): + if hasattr(renderer, k): + setattr(renderer, k, v) + + renderer.render(fig_dict) + + +# Make renderers a singleton object +# --------------------------------- +renderers = RenderersConfig() +del RenderersConfig + + +# Show +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 + + 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. + + width: int or float + An integer or float that determines the number of pixels wide the + plot is. The default is set in plotly.js. + + height: int or float + An integer or float specifying the height of the plot in pixels. + The default is set in plotly.js. + + config: dict + A dict of parameters to configure the figure. The defaults are set + in plotly.js. + + Returns + ------- + None + """ + fig_dict = validate_coerce_fig_to_dict(fig, validate) + + # Mimetype renderers + bundle = renderers._build_mime_bundle(fig_dict, renderers_string=renderer, **kwargs) + if bundle: + if not ipython_display: + raise ValueError( + "Mime type rendering requires ipython but it is not installed" + ) + + if not nbformat or Version(nbformat.__version__) < Version("4.2.0"): + raise ValueError( + "Mime type rendering requires nbformat>=4.2.0 but it is not installed" + ) + + display_jupyter_version_warnings() + + ipython_display.display(bundle, raw=True) + + # external renderers + renderers._perform_external_rendering(fig_dict, renderers_string=renderer, **kwargs) + + +# Register renderers +# ------------------ + +# Plotly mime type +plotly_renderer = PlotlyRenderer() +renderers["plotly_mimetype"] = plotly_renderer +renderers["jupyterlab"] = plotly_renderer +renderers["nteract"] = plotly_renderer +renderers["vscode"] = plotly_renderer + +# HTML-based +config = {} +renderers["notebook"] = NotebookRenderer(config=config) +renderers["notebook_connected"] = NotebookRenderer(config=config, connected=True) +renderers["kaggle"] = KaggleRenderer(config=config) +renderers["azure"] = AzureRenderer(config=config) +renderers["colab"] = ColabRenderer(config=config) +renderers["cocalc"] = CoCalcRenderer() +renderers["databricks"] = DatabricksRenderer() + +# JSON +renderers["json"] = JsonRenderer() + +# Static Image +renderers["png"] = PngRenderer() +jpeg_renderer = JpegRenderer() +renderers["jpeg"] = jpeg_renderer +renderers["jpg"] = jpeg_renderer +renderers["svg"] = SvgRenderer() +renderers["pdf"] = PdfRenderer() + +# External +renderers["browser"] = BrowserRenderer(config=config) +renderers["firefox"] = BrowserRenderer(config=config, using=("firefox")) +renderers["chrome"] = BrowserRenderer(config=config, using=("chrome", "google-chrome")) +renderers["chromium"] = BrowserRenderer( + config=config, using=("chromium", "chromium-browser") +) +renderers["iframe"] = IFrameRenderer(config=config, include_plotlyjs=True) +renderers["iframe_connected"] = IFrameRenderer(config=config, include_plotlyjs="cdn") +renderers["sphinx_gallery"] = SphinxGalleryHtmlRenderer() +renderers["sphinx_gallery_png"] = SphinxGalleryOrcaRenderer() + +# Set default renderer +# -------------------- +# Version 4 renderer configuration +default_renderer = None + +# Handle the PLOTLY_RENDERER environment variable +env_renderer = os.environ.get("PLOTLY_RENDERER", None) +if env_renderer: + try: + renderers._validate_coerce_renderers(env_renderer) + except ValueError: + raise ValueError( + """ +Invalid named renderer(s) specified in the 'PLOTLY_RENDERER' +environment variable: {env_renderer}""".format(env_renderer=env_renderer) + ) + + default_renderer = env_renderer +elif ipython: + # Try to detect environment so that we can enable a useful + # default renderer + if not default_renderer: + try: + import google.colab # noqa: F401 + + 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 an Azure Notebook + if not default_renderer and "AZURE_NOTEBOOKS_HOST" in os.environ: + default_renderer = "azure" + + # Check if we're running in VSCode + if not default_renderer and "VSCODE_PID" in os.environ: + default_renderer = "vscode" + + # Check if we're running in nteract + if not default_renderer and "NTERACT_EXE" in os.environ: + default_renderer = "nteract" + + # Check if we're running in CoCalc + if not default_renderer and "COCALC_PROJECT_ID" in os.environ: + default_renderer = "cocalc" + + if not default_renderer and "DATABRICKS_RUNTIME_VERSION" in os.environ: + default_renderer = "databricks" + + # Check if we're running in spyder and orca is installed + if not default_renderer and "SPYDER_ARGS" in os.environ: + try: + from plotly.io.orca import validate_executable + + validate_executable() + default_renderer = "svg" + except ValueError: + # orca not found + pass + + # Check if we're running in ipython terminal + ipython_info = ipython.get_ipython() + shell = ipython_info.__class__.__name__ + if not default_renderer and (shell == "TerminalInteractiveShell"): + default_renderer = "browser" + + # Check if we're running in a Jupyter notebook or JupyterLab + if ( + not default_renderer + and (shell == "ZMQInteractiveShell") + and (type(ipython_info).__module__.startswith("ipykernel.")) + ): + default_renderer = "plotly_mimetype" + + # Fallback to renderer combination that will work automatically + # in the jupyter notebook, jupyterlab, nteract, vscode, and + # nbconvert HTML export. + if not default_renderer: + default_renderer = "plotly_mimetype+notebook" +else: + # If ipython isn't available, try to display figures in the default + # browser + try: + import webbrowser + + webbrowser.get() + default_renderer = "browser" + except Exception: + # Many things could have gone wrong + # There could not be a webbrowser Python module, + # or the module may be a dumb placeholder + pass + +renderers.render_on_display = True +renderers.default = default_renderer diff --git a/venv/lib/python3.8/site-packages/plotly/io/_sg_scraper.py b/venv/lib/python3.8/site-packages/plotly/io/_sg_scraper.py new file mode 100644 index 0000000..af15b7d --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/_sg_scraper.py @@ -0,0 +1,100 @@ +# This module defines an image scraper for sphinx-gallery +# https://sphinx-gallery.github.io/ +# which can be used by projects using plotly in their documentation. +from glob import glob +import os +import shutil + +import plotly + +plotly.io.renderers.default = "sphinx_gallery_png" + + +def plotly_sg_scraper(block, block_vars, gallery_conf, **kwargs): + """Scrape Plotly figures for galleries of examples using + sphinx-gallery. + + Examples should use ``plotly.io.show()`` to display the figure with + the custom sphinx_gallery renderer. + + Since the sphinx_gallery renderer generates both html and static png + files, we simply crawl these files and give them the appropriate path. + + Parameters + ---------- + block : tuple + A tuple containing the (label, content, line_number) of the block. + block_vars : dict + Dict of block variables. + gallery_conf : dict + Contains the configuration of Sphinx-Gallery + **kwargs : dict + Additional keyword arguments to pass to + :meth:`~matplotlib.figure.Figure.savefig`, e.g. ``format='svg'``. + The ``format`` kwarg in particular is used to set the file extension + of the output file (currently only 'png' and 'svg' are supported). + + Returns + ------- + rst : str + The ReSTructuredText that will be rendered to HTML containing + the images. + + Notes + ----- + Add this function to the image scrapers + """ + examples_dir = os.path.dirname(block_vars["src_file"]) + pngs = sorted(glob(os.path.join(examples_dir, "*.png"))) + htmls = sorted(glob(os.path.join(examples_dir, "*.html"))) + image_path_iterator = block_vars["image_path_iterator"] + image_names = list() + seen = set() + for html, png in zip(htmls, pngs): + if png not in seen: + seen |= set(png) + this_image_path_png = next(image_path_iterator) + this_image_path_html = os.path.splitext(this_image_path_png)[0] + ".html" + image_names.append(this_image_path_html) + shutil.move(png, this_image_path_png) + shutil.move(html, this_image_path_html) + # Use the `figure_rst` helper function to generate rST for image files + return figure_rst(image_names, gallery_conf["src_dir"]) + + +def figure_rst(figure_list, sources_dir): + """Generate RST for a list of PNG filenames. + + Depending on whether we have one or more figures, we use a + single rst call to 'image' or a horizontal list. + + Parameters + ---------- + figure_list : list + List of strings of the figures' absolute paths. + sources_dir : str + absolute path of Sphinx documentation sources + + Returns + ------- + images_rst : str + rst code to embed the images in the document + """ + + figure_paths = [ + os.path.relpath(figure_path, sources_dir).replace(os.sep, "/").lstrip("/") + for figure_path in figure_list + ] + images_rst = "" + if not figure_paths: + return images_rst + figure_name = figure_paths[0] + figure_path = os.path.join("images", os.path.basename(figure_name)) + images_rst = SINGLE_HTML % figure_path + return images_rst + + +SINGLE_HTML = """ +.. raw:: html + :file: %s +""" diff --git a/venv/lib/python3.8/site-packages/plotly/io/_templates.py b/venv/lib/python3.8/site-packages/plotly/io/_templates.py new file mode 100644 index 0000000..160ee7c --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/_templates.py @@ -0,0 +1,492 @@ +import textwrap +import pkgutil + +import copy +import os +import json +from functools import reduce + +try: + from math import gcd +except ImportError: + # Python 2 + from fractions import gcd + +# Create Lazy sentinal object to indicate that a template should be loaded +# on-demand from package_data +Lazy = object() + + +# Templates configuration class +# ----------------------------- +class TemplatesConfig(object): + """ + Singleton object containing the current figure templates (aka themes) + """ + + def __init__(self): + # Initialize properties dict + self._templates = {} + + # Initialize built-in templates + default_templates = [ + "ggplot2", + "seaborn", + "simple_white", + "plotly", + "plotly_white", + "plotly_dark", + "presentation", + "xgridoff", + "ygridoff", + "gridon", + "none", + ] + + for template_name in default_templates: + self._templates[template_name] = Lazy + + self._validator = None + self._default = None + + # ### Magic methods ### + # Make this act as a dict of templates + def __len__(self): + return len(self._templates) + + def __contains__(self, item): + return item in self._templates + + def __iter__(self): + return iter(self._templates) + + def __getitem__(self, item): + if isinstance(item, str): + template_names = item.split("+") + else: + template_names = [item] + + templates = [] + for template_name in template_names: + template = self._templates[template_name] + if template is Lazy: + from plotly.graph_objs.layout import Template + + if template_name == "none": + # "none" is a special built-in named template that applied no defaults + template = Template(data_scatter=[{}]) + self._templates[template_name] = template + else: + # Load template from package data + path = os.path.join( + "package_data", "templates", template_name + ".json" + ) + template_str = pkgutil.get_data("plotly", path).decode("utf-8") + template_dict = json.loads(template_str) + template = Template(template_dict, _validate=False) + + self._templates[template_name] = template + templates.append(self._templates[template_name]) + + return self.merge_templates(*templates) + + def __setitem__(self, key, value): + self._templates[key] = self._validate(value) + + def __delitem__(self, key): + # Remove template + del self._templates[key] + + # Check if we need to remove it as the default + if self._default == key: + self._default = None + + def _validate(self, value): + if not self._validator: + from plotly.validator_cache import ValidatorCache + + self._validator = ValidatorCache.get_validator("layout", "template") + + return self._validator.validate_coerce(value) + + def keys(self): + return self._templates.keys() + + def items(self): + return self._templates.items() + + def update(self, d={}, **kwargs): + """ + Update one or more templates from a dict or from input keyword + arguments. + + Parameters + ---------- + d: dict + Dictionary from template names to new template values. + + kwargs + Named argument value pairs where the name is a template name + and the value is a new template value. + """ + for k, v in dict(d, **kwargs).items(): + self[k] = v + + # ### Properties ### + @property + def default(self): + """ + The name of the default template, or None if no there is no default + + If not None, the default template is automatically applied to all + figures during figure construction if no explicit template is + specified. + + The names of available templates may be retrieved with: + + >>> import plotly.io as pio + >>> list(pio.templates) + + Returns + ------- + str + """ + return self._default + + @default.setter + def default(self, value): + # Validate value + # Could be a Template object, the key of a registered template, + # Or a string containing the names of multiple templates joined on + # '+' characters + self._validate(value) + self._default = value + + def __repr__(self): + return """\ +Templates configuration +----------------------- + Default template: {default} + Available templates: +{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 merge_templates(self, *args): + """ + Merge a collection of templates into a single combined template. + Templates are process from left to right so if multiple templates + specify the same propery, the right-most template will take + precedence. + + Parameters + ---------- + args: list of Template + Zero or more template objects (or dicts with compatible properties) + + Returns + ------- + template: + A combined template object + + Examples + -------- + + >>> pio.templates.merge_templates( + ... go.layout.Template(layout={'font': {'size': 20}}), + ... go.layout.Template(data={'scatter': [{'mode': 'markers'}]}), + ... go.layout.Template(layout={'font': {'family': 'Courier'}})) + layout.Template({ + 'data': {'scatter': [{'mode': 'markers', 'type': 'scatter'}]}, + 'layout': {'font': {'family': 'Courier', 'size': 20}} + }) + """ + if args: + return reduce(self._merge_2_templates, args) + else: + from plotly.graph_objs.layout import Template + + return Template() + + def _merge_2_templates(self, template1, template2): + """ + Helper function for merge_templates that merges exactly two templates + + Parameters + ---------- + template1: Template + template2: Template + + Returns + ------- + Template: + merged template + """ + # Validate/copy input templates + result = self._validate(template1) + other = self._validate(template2) + + # Cycle traces + for trace_type in result.data: + result_traces = result.data[trace_type] + other_traces = other.data[trace_type] + + if result_traces and other_traces: + lcm = ( + len(result_traces) + * len(other_traces) + // gcd(len(result_traces), len(other_traces)) + ) + + # Cycle result traces + result.data[trace_type] = result_traces * (lcm // len(result_traces)) + + # Cycle other traces + other.data[trace_type] = other_traces * (lcm // len(other_traces)) + + # Perform update + result.update(other) + + return result + + +# Make config a singleton object +# ------------------------------ +templates = TemplatesConfig() +del TemplatesConfig + + +# Template utilities +# ------------------ +def walk_push_to_template(fig_obj, template_obj, skip): + """ + Move style properties from fig_obj to template_obj. + + Parameters + ---------- + fig_obj: plotly.basedatatypes.BasePlotlyType + template_obj: plotly.basedatatypes.BasePlotlyType + skip: set of str + Set of names of properties to skip + """ + from _plotly_utils.basevalidators import ( + CompoundValidator, + CompoundArrayValidator, + is_array, + ) + + for prop in list(fig_obj._props): + if prop == "template" or prop in skip: + # Avoid infinite recursion + continue + + fig_val = fig_obj[prop] + template_val = template_obj[prop] + + validator = fig_obj._get_validator(prop) + + if isinstance(validator, CompoundValidator): + walk_push_to_template(fig_val, template_val, skip) + if not fig_val._props: + # Check if we can remove prop itself + fig_obj[prop] = None + elif isinstance(validator, CompoundArrayValidator) and fig_val: + template_elements = list(template_val) + template_element_names = [el.name for el in template_elements] + template_propdefaults = template_obj[prop[:-1] + "defaults"] + + for fig_el in fig_val: + element_name = fig_el.name + if element_name: + # No properties are skipped inside a named array element + skip = set() + if fig_el.name in template_element_names: + item_index = template_element_names.index(fig_el.name) + template_el = template_elements[item_index] + walk_push_to_template(fig_el, template_el, skip) + else: + template_el = fig_el.__class__() + walk_push_to_template(fig_el, template_el, skip) + template_elements.append(template_el) + template_element_names.append(fig_el.name) + + # Restore element name + # since it was pushed to template above + fig_el.name = element_name + else: + walk_push_to_template(fig_el, template_propdefaults, skip) + + template_obj[prop] = template_elements + + elif not validator.array_ok or not is_array(fig_val): + # Move property value from figure to template + template_obj[prop] = fig_val + try: + fig_obj[prop] = None + except ValueError: + # Property cannot be set to None, move on. + pass + + +def to_templated(fig, skip=("title", "text")): + """ + Return a copy of a figure where all styling properties have been moved + into the figure's template. The template property of the resulting figure + may then be used to set the default styling of other figures. + + Parameters + ---------- + fig + Figure object or dict representing a figure + skip + A collection of names of properties to skip when moving properties to + the template. Defaults to ('title', 'text') so that the text + of figure titles, axis titles, and annotations does not become part of + the template + + Examples + -------- + Imports + + >>> import plotly.graph_objs as go + >>> import plotly.io as pio + + Construct a figure with large courier text + + >>> fig = go.Figure(layout={'title': 'Figure Title', + ... 'font': {'size': 20, 'family': 'Courier'}, + ... 'template':"none"}) + >>> fig # doctest: +NORMALIZE_WHITESPACE + Figure({ + 'data': [], + 'layout': {'font': {'family': 'Courier', 'size': 20}, + 'template': '...', 'title': {'text': 'Figure Title'}} + }) + + Convert to a figure with a template. Note how the 'font' properties have + been moved into the template property. + + >>> templated_fig = pio.to_templated(fig) + >>> templated_fig.layout.template + layout.Template({ + 'layout': {'font': {'family': 'Courier', 'size': 20}} + }) + >>> templated_fig + Figure({ + 'data': [], 'layout': {'template': '...', 'title': {'text': 'Figure Title'}} + }) + + + Next create a new figure with this template + + >>> fig2 = go.Figure(layout={ + ... 'title': 'Figure 2 Title', + ... 'template': templated_fig.layout.template}) + >>> fig2.layout.template + layout.Template({ + 'layout': {'font': {'family': 'Courier', 'size': 20}} + }) + + The default font in fig2 will now be size 20 Courier. + + Next, register as a named template... + + >>> pio.templates['large_courier'] = templated_fig.layout.template + + and specify this template by name when constructing a figure. + + >>> go.Figure(layout={ + ... 'title': 'Figure 3 Title', + ... 'template': 'large_courier'}) # doctest: +ELLIPSIS + Figure(...) + + Finally, set this as the default template to be applied to all new figures + + >>> pio.templates.default = 'large_courier' + >>> fig = go.Figure(layout={'title': 'Figure 4 Title'}) + >>> fig.layout.template + layout.Template({ + 'layout': {'font': {'family': 'Courier', 'size': 20}} + }) + + Returns + ------- + go.Figure + """ + + # process fig + from plotly.basedatatypes import BaseFigure + from plotly.graph_objs import Figure + + if not isinstance(fig, BaseFigure): + fig = Figure(fig) + + # Process skip + if not skip: + skip = set() + else: + skip = set(skip) + + # Always skip uids + skip.add("uid") + + # Initialize templated figure with deep copy of input figure + templated_fig = copy.deepcopy(fig) + + # Handle layout + walk_push_to_template( + templated_fig.layout, templated_fig.layout.template.layout, skip=skip + ) + + # Handle traces + trace_type_indexes = {} + for trace in list(templated_fig.data): + template_index = trace_type_indexes.get(trace.type, 0) + + # Extend template traces if necessary + template_traces = list(templated_fig.layout.template.data[trace.type]) + while len(template_traces) <= template_index: + # Append empty trace + template_traces.append(trace.__class__()) + + # Get corresponding template trace + template_trace = template_traces[template_index] + + # Perform push properties to template + walk_push_to_template(trace, template_trace, skip=skip) + + # Update template traces in templated_fig + templated_fig.layout.template.data[trace.type] = template_traces + + # Update trace_type_indexes + trace_type_indexes[trace.type] = template_index + 1 + + # Remove useless trace arrays + any_non_empty = False + for trace_type in templated_fig.layout.template.data: + traces = templated_fig.layout.template.data[trace_type] + is_empty = [trace.to_plotly_json() == {"type": trace_type} for trace in traces] + if all(is_empty): + templated_fig.layout.template.data[trace_type] = None + else: + any_non_empty = True + + # Check if we can remove the data altogether key + if not any_non_empty: + templated_fig.layout.template.data = None + + return templated_fig diff --git a/venv/lib/python3.8/site-packages/plotly/io/_utils.py b/venv/lib/python3.8/site-packages/plotly/io/_utils.py new file mode 100644 index 0000000..4d27e03 --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/_utils.py @@ -0,0 +1,93 @@ +from typing import List + +import plotly +import plotly.graph_objs as go +from plotly.offline import get_plotlyjs_version + + +def validate_coerce_fig_to_dict(fig, validate): + from plotly.basedatatypes import BaseFigure + + if isinstance(fig, BaseFigure): + fig_dict = fig.to_dict() + elif isinstance(fig, dict): + if validate: + # This will raise an exception if fig is not a valid plotly figure + fig_dict = plotly.graph_objs.Figure(fig).to_plotly_json() + else: + fig_dict = fig + elif hasattr(fig, "to_plotly_json"): + fig_dict = fig.to_plotly_json() + else: + raise ValueError( + """ +The fig parameter must be a dict or Figure. + Received value of type {typ}: {v}""".format(typ=type(fig), v=fig) + ) + return fig_dict + + +def validate_coerce_output_type(output_type): + if output_type == "Figure" or output_type == go.Figure: + cls = go.Figure + elif output_type == "FigureWidget" or ( + hasattr(go, "FigureWidget") and output_type == go.FigureWidget + ): + cls = go.FigureWidget + else: + raise ValueError( + """ +Invalid output type: {output_type} + Must be one of: 'Figure', 'FigureWidget'""" + ) + return cls + + +def broadcast_args_to_dicts(**kwargs: dict) -> List[dict]: + """ + Given one or more keyword arguments which may be either a single value or a list of values, + return a list of keyword dictionaries by broadcasting the single valuesacross all the dicts. + If more than one item in the input is a list, all lists must be the same length. + + Parameters + ---------- + **kwargs: dict + The keyword arguments + + Returns + ------- + list of dicts + A list of dictionaries + + Raises + ------ + ValueError + If any of the input lists are not the same length + """ + # Check that all list arguments have the same length, + # and find out what that length is + # If there are no list arguments, length is 1 + list_lengths = [len(v) for v in tuple(kwargs.values()) if isinstance(v, list)] + if list_lengths and len(set(list_lengths)) > 1: + raise ValueError("All list arguments must have the same length.") + list_length = list_lengths[0] if list_lengths else 1 + + # Expand all arguments to lists of the same length + expanded_kwargs = { + k: [v] * list_length if not isinstance(v, list) else v + for k, v in kwargs.items() + } + # Reshape into a list of dictionaries + # Each dictionary represents the keyword arguments for a single function call + list_of_kwargs = [ + {k: v[i] for k, v in expanded_kwargs.items()} for i in range(list_length) + ] + + return list_of_kwargs + + +def plotly_cdn_url(cdn_ver=get_plotlyjs_version()): + """Return a valid plotly CDN url.""" + return "https://cdn.plot.ly/plotly-{cdn_ver}.min.js".format( + cdn_ver=cdn_ver, + ) diff --git a/venv/lib/python3.8/site-packages/plotly/io/base_renderers.py b/venv/lib/python3.8/site-packages/plotly/io/base_renderers.py new file mode 100644 index 0000000..78c1d86 --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/base_renderers.py @@ -0,0 +1,17 @@ +# ruff: noqa: F401 +from ._base_renderers import ( + MimetypeRenderer, + PlotlyRenderer, + JsonRenderer, + ImageRenderer, + PngRenderer, + SvgRenderer, + PdfRenderer, + JpegRenderer, + HtmlRenderer, + ColabRenderer, + KaggleRenderer, + NotebookRenderer, + ExternalRenderer, + BrowserRenderer, +) diff --git a/venv/lib/python3.8/site-packages/plotly/io/json.py b/venv/lib/python3.8/site-packages/plotly/io/json.py new file mode 100644 index 0000000..86c320d --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/json.py @@ -0,0 +1,10 @@ +# ruff: noqa: F401 +from ._json import ( + to_json, + write_json, + from_json, + read_json, + config, + to_json_plotly, + from_json_plotly, +) diff --git a/venv/lib/python3.8/site-packages/plotly/io/kaleido.py b/venv/lib/python3.8/site-packages/plotly/io/kaleido.py new file mode 100644 index 0000000..c086ea3 --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/kaleido.py @@ -0,0 +1,12 @@ +# ruff: noqa: F401 +from ._kaleido import ( + to_image, + write_image, + scope, + kaleido_available, + kaleido_major, + ENABLE_KALEIDO_V0_DEPRECATION_WARNINGS, + KALEIDO_DEPRECATION_MSG, + ORCA_DEPRECATION_MSG, + ENGINE_PARAM_DEPRECATION_MSG, +) diff --git a/venv/lib/python3.8/site-packages/plotly/io/orca.py b/venv/lib/python3.8/site-packages/plotly/io/orca.py new file mode 100644 index 0000000..4fd5c19 --- /dev/null +++ b/venv/lib/python3.8/site-packages/plotly/io/orca.py @@ -0,0 +1,9 @@ +# ruff: noqa: F401 +from ._orca import ( + ensure_server, + shutdown_server, + validate_executable, + reset_status, + config, + status, +) |