This commit is contained in:
2025-09-07 22:09:54 +02:00
parent e1b817252c
commit 2fc0d000b6
7796 changed files with 2159515 additions and 933 deletions

View File

@ -0,0 +1,70 @@
# 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, get_chrome
__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",
"get_chrome",
]
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",
"._kaleido.get_chrome",
],
)
# Set default template (for < 3.7 this is done in ploty/__init__.py)
from plotly.io import templates
templates._default = "plotly"

View File

@ -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=None):
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=None):
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=None):
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=None):
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)."
)

View File

@ -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()

View File

@ -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)

View File

@ -0,0 +1,598 @@
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.
Note: A figure converted to JSON with one version of Plotly.py may not be compatible with another version.
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.
Note: A figure converted to JSON with one version of Plotly.py may not be compatible with another version.
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

View File

@ -0,0 +1,900 @@
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 plotly_get_chrome() -> None:
"""
Install Google Chrome for Kaleido (Required for Plotly image export).
This function is a command-line wrapper for `plotly.io.get_chrome()`.
When running from the command line, use the command `plotly_get_chrome`;
when calling from Python code, use `plotly.io.get_chrome()`.
"""
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
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)
# 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 = get_chrome(chrome_install_path)
print("Chrome installed successfully.")
print(f"The Chrome executable is now located at: {exe_path}")
def get_chrome(path: Union[str, Path, None] = None) -> Path:
"""
Get the path to the Chrome executable for Kaleido.
This function is used by the `plotly_get_chrome` command line utility.
Parameters
----------
path: str or Path or None
The path to the directory where Chrome should be installed.
If None, the default download path will be used.
"""
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]'`."
"""
)
# Use default download path if no path was specified
if path:
user_specified_path = True
chrome_install_path = Path(path) # Ensure it's a Path object
else:
user_specified_path = False
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.
"""
)
return kaleido.get_chrome_sync(path=chrome_install_path)
__all__ = ["to_image", "write_image", "scope", "full_figure_for_development"]

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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
"""

View File

@ -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

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -0,0 +1,9 @@
# ruff: noqa: F401
from ._orca import (
ensure_server,
shutdown_server,
validate_executable,
reset_status,
config,
status,
)