Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Kaleido v1 in Plotly.py #5062

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
45 changes: 45 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,35 @@ commands:
source .venv/bin/activate
python -m pytest -x test_init/test_lazy_imports.py

test_io_kaleido_v1:
steps:
- checkout
- browser-tools/install-chrome
- browser-tools/install-chromedriver
- run:
name: Install dependencies
command: |
curl -LsSf https://astral.sh/uv/install.sh | sh
uv venv
source .venv/bin/activate
uv pip install .
uv pip install -r ./test_requirements/requirements_optional.txt
# Install Kaleido v1 instead of the default version
uv pip uninstall -y kaleido
uv pip install 'git+https://github.com/plotly/[email protected]#subdirectory=src/py'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uv pip install kaleido>=1.0.0 || uv pip install 'git+https://github.com/plotly/Kaleido.git@latest-tag#subdirectory=src/py' is my recommendation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I'll just update the CI manually once Kaleido 1.0.0 full version is on PyPI.

- run:
name: List installed packages and python version
command: |
source .venv/bin/activate
uv pip list
python --version
- run:
name: Test plotly.io with Kaleido v1
command: |
source .venv/bin/activate
python -m pytest tests/test_io
no_output_timeout: 20m

jobs:
check-code-formatting:
docker:
Expand Down Expand Up @@ -166,6 +195,17 @@ jobs:
pandas_version: <<parameters.pandas_version>>
numpy_version: <<parameters.numpy_version>>

test_kaleido_v1:
parameters:
python_version:
default: "3.12"
type: string
executor:
name: docker-container
python_version: <<parameters.python_version>>
steps:
- test_io_kaleido_v1

# Percy
python_311_percy:
docker:
Expand Down Expand Up @@ -448,5 +488,10 @@ workflows:
python_version: "3.9"
pandas_version: "1.2.4"
numpy_version: "1.26.4"
- test_kaleido_v1:
matrix:
parameters:
python_version:
- "3.12"
- python_311_percy
- build-doc
172 changes: 76 additions & 96 deletions plotly/io/_kaleido.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,41 @@
import os
import json
from pathlib import Path
import importlib.metadata as importlib_metadata
from packaging.version import Version
import tempfile

import plotly
from plotly.io._utils import validate_coerce_fig_to_dict

try:
from kaleido.scopes.plotly import PlotlyScope

scope = PlotlyScope()

# 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:
scope.mathjax = (
"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js"
)
except ImportError:
import kaleido

kaleido_available = True
kaleido_major = Version(importlib_metadata.version("kaleido")).major

if kaleido_major < 1:
# Kaleido v0
from kaleido.scopes.plotly import PlotlyScope

scope = PlotlyScope()
# 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:
scope.mathjax = (
"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js"
)
except ImportError as e:
kaleido_available = False
kaleido_major = -1
PlotlyScope = None
scope = None


def to_image(
fig, format=None, width=None, height=None, scale=None, validate=True, engine="auto"
fig, format=None, width=None, height=None, scale=None, validate=True, engine=None
):
"""
Convert a figure to a static image bytes string
Expand All @@ -35,100 +47,52 @@ def to_image(

format: str or None
The desired image format. One of
- 'png'
- 'jpg' or 'jpeg'
- 'webp'
- 'svg'
- 'pdf'
- 'eps' (Requires the poppler library to be installed and on the PATH)
- 'png'
- 'jpg' or 'jpeg'
- 'webp'
- 'svg'
- 'pdf'
- 'eps' (Requires the poppler library to be installed and on the PATH)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we still handle EPS?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gvwilson Following up on this — Kaleido v1 does not support EPS yet. So either we drop support for EPS entirely, or document that EPS is only available with Kaleido v0 and add an informative error message.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please do the latter - thanks


If not specified, will default to:
- `plotly.io.kaleido.scope.default_format` if engine is "kaleido"
- `plotly.io.orca.config.default_format` if engine is "orca"
If not specified, will default to `plotly.io.kaleido.scope.default_format`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these references to plotly.io.kaleido.scope.. need to be something else if that's no longer imported here
https://github.com/plotly/plotly.py/pull/5062/files#diff-40813ac13aafeaa7135da57a86bec52cae638135b0acf9324677c9c4eee2ba2bR1

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, those docstrings should be changed -- I need to research where those defaults are defined in Kaleido v1. (@ayjayt do you know off the top of your head?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/plotly/Kaleido/blob/d493b9dc0c04940721dc2d67e8b342d31fc6bb18/src/py/kaleido/_fig_tools.py#L9-L13

The order is png default overridden by user-specified output path w/ extension overriden by user-specified format in opts


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.kaleido.scope.default_width` if engine is "kaleido"
- `plotly.io.orca.config.default_width` if engine is "orca"
If not specified, will default to `plotly.io.kaleido.scope.default_width`

height: int or None
The height of the exported image in layout pixels. If the `scale`
property is 1.0, this will also be the height of the exported image
in physical pixels.

If not specified, will default to:
- `plotly.io.kaleido.scope.default_height` if engine is "kaleido"
- `plotly.io.orca.config.default_height` if engine is "orca"
If not specified, will default to `plotly.io.kaleido.scope.default_height`

scale: int or float or None
The scale factor to use when exporting the figure. A scale factor
larger than 1.0 will increase the image resolution with respect
to the figure's layout pixel dimensions. Whereas as scale factor of
less than 1.0 will decrease the image resolution.

If not specified, will default to:
- `plotly.io.kaleido.scope.default_scale` if engine is "kaleido"
- `plotly.io.orca.config.default_scale` if engine is "orca"

If not specified, will default to `plotly.io.kaleido.scope.default_scale`

validate: bool
True if the figure should be validated before being converted to
an image, False otherwise.

engine: str
Image export engine to use:
- "kaleido": Use Kaleido for image export
- "orca": Use Orca for image export
- "auto" (default): Use Kaleido if installed, otherwise use orca
engine (deprecated): str
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we print a deprecation warning if this argument is used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet, assuming we are in agreement about removing this argument, I'll add one

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm in agreement that we should remove this argument

No longer used. Kaleido is the only supported engine.

Returns
-------
bytes
The image data
"""
# Handle engine
# -------------
if engine == "auto":
if scope is not None:
# Default to kaleido if available
engine = "kaleido"
else:
# See if orca is available
from ._orca import validate_executable

try:
validate_executable()
engine = "orca"
except:
# If orca not configured properly, make sure we display the error
# message advising the installation of kaleido
engine = "kaleido"

if engine == "orca":
# 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(
"Invalid image export engine specified: {engine}".format(
engine=repr(engine)
)
)

# Raise informative error message if Kaleido is not installed
if scope is None:
if not kaleido_available:
raise ValueError(
"""
Image export using the "kaleido" engine requires the kaleido package,
Expand All @@ -137,12 +101,27 @@ def to_image(
"""
)

# Validate figure
# ---------------
# Convert figure to dict (and validate if requested)
fig_dict = validate_coerce_fig_to_dict(fig, validate)
img_bytes = scope.transform(
fig_dict, format=format, width=width, height=height, scale=scale
)

# Request image bytes
if kaleido_major > 0:
# Kaleido v1
img_bytes = kaleido.calc_fig_sync(
fig_dict,
path=None,
opts=dict(
format=format,
width=width,
height=height,
scale=scale,
),
)
else:
# Kaleido v0
img_bytes = scope.transform(
fig_dict, format=format, width=width, height=height, scale=scale
)

return img_bytes

Expand Down Expand Up @@ -190,38 +169,29 @@ def write_image(
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.kaleido.scope.default_width` if engine is "kaleido"
- `plotly.io.orca.config.default_width` if engine is "orca"
If not specified, will default to`plotly.io.kaleido.scope.default_width`

height: int or None
The height of the exported image in layout pixels. If the `scale`
property is 1.0, this will also be the height of the exported image
in physical pixels.

If not specified, will default to:
- `plotly.io.kaleido.scope.default_height` if engine is "kaleido"
- `plotly.io.orca.config.default_height` if engine is "orca"
If not specified, will default to `plotly.io.kaleido.scope.default_height`

scale: int or float or None
The scale factor to use when exporting the figure. A scale factor
larger than 1.0 will increase the image resolution with respect
to the figure's layout pixel dimensions. Whereas as scale factor of
less than 1.0 will decrease the image resolution.

If not specified, will default to:
- `plotly.io.kaleido.scope.default_scale` if engine is "kaleido"
- `plotly.io.orca.config.default_scale` if engine is "orca"
If not specified, will default to `plotly.io.kaleido.scope.default_scale`

validate: bool
True if the figure should be validated before being converted to
an image, False otherwise.

engine: str
Image export engine to use:
- "kaleido": Use Kaleido for image export
- "orca": Use Orca for image export
- "auto" (default): Use Kaleido if installed, otherwise use orca
engine (deprecated): str
No longer used. Kaleido is the only supported engine.

Returns
-------
Expand Down Expand Up @@ -323,7 +293,7 @@ def full_figure_for_development(fig, warn=True, as_dict=False):
"""

# Raise informative error message if Kaleido is not installed
if scope is None:
if not kaleido_available:
raise ValueError(
"""
Full figure generation requires the kaleido package,
Expand All @@ -341,7 +311,17 @@ def full_figure_for_development(fig, warn=True, as_dict=False):
"To suppress this warning, set warn=False"
)

fig = json.loads(scope.transform(fig, format="json").decode("utf-8"))
if 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
fig = json.loads(scope.transform(fig, format="json").decode("utf-8"))

if as_dict:
return fig
else:
Expand Down
2 changes: 1 addition & 1 deletion plotly/io/kaleido.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from ._kaleido import to_image, write_image, scope
from ._kaleido import write_image, to_image
1 change: 1 addition & 0 deletions test_requirements/requirements_optional.txt
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ polars[timezone]
pyarrow
plotly-geo
vaex;python_version<="3.9"
pdfrw
Loading