Skip to content

Commit aac9c20

Browse files
committed
refactor write_images() to use kaleido.write_fig_from_object_sync()
1 parent 692842f commit aac9c20

File tree

2 files changed

+67
-62
lines changed

2 files changed

+67
-62
lines changed

plotly/io/_kaleido.py

Lines changed: 57 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,17 @@
66
import warnings
77

88
import plotly
9-
from plotly.io._utils import validate_coerce_fig_to_dict, as_individual_args
9+
from plotly.io._utils import validate_coerce_fig_to_dict, broadcast_args_to_dicts
1010
from plotly.io import defaults
1111

1212
ENGINE_SUPPORT_TIMELINE = "September 2025"
1313

14+
PLOTLY_GET_CHROME_ERROR_MSG = """
15+
16+
Kaleido requires Google Chrome to be installed. Install it by running:
17+
$ plotly_get_chrome
18+
"""
19+
1420

1521
# TODO: Remove --pre flag once Kaleido v1 full release is available
1622
KALEIDO_DEPRECATION_MSG = f"""
@@ -158,6 +164,7 @@ def as_path_object(file: str | Path) -> Path | None:
158164
path = None
159165
return path
160166

167+
161168
def infer_format(path: Path | None, format: str | None) -> str | None:
162169
if path is not None and format is None:
163170
ext = path.suffix
@@ -177,7 +184,6 @@ def infer_format(path: Path | None, format: str | None) -> str | None:
177184
return format
178185

179186

180-
181187
def to_image(
182188
fig,
183189
format=None,
@@ -331,13 +337,7 @@ def to_image(
331337
),
332338
)
333339
except choreographer.errors.ChromeNotFoundError:
334-
raise RuntimeError(
335-
"""
336-
337-
Kaleido requires Google Chrome to be installed. Install it by running:
338-
$ plotly_get_chrome
339-
"""
340-
)
340+
raise RuntimeError(PLOTLY_GET_CHROME_ERROR_MSG)
341341

342342
else:
343343
# Kaleido v0
@@ -481,7 +481,7 @@ def write_image(
481481

482482

483483
def write_images(
484-
figs,
484+
fig,
485485
file,
486486
format=None,
487487
scale=None,
@@ -494,13 +494,17 @@ def write_images(
494494
calling write_image() multiple times. This function can only be used with the Kaleido
495495
engine, v1.0.0 or greater.
496496
497+
This function accepts the same arguments as write_image() (minus the `engine` argument),
498+
except that any of the arguments may be either a single value or an iterable of values.
499+
If multiple arguments are iterable, they must all have the same length.
500+
497501
Parameters
498502
----------
499-
figs:
503+
fig:
500504
Iterable of figure objects or dicts representing a figure
501505
502-
directory: str or writeable
503-
A string or pathlib.Path object representing a local directory path.
506+
file: str or writeable
507+
Iterables of strings or pathlib.Path objects representing local file paths to write to.
504508
505509
format: str or None
506510
The desired image format. One of
@@ -562,39 +566,48 @@ def write_images(
562566
"""
563567
)
564568

565-
# Try to cast `file` as a pathlib object `path`.
566-
path = as_path_object(file)
567-
568-
# Infer image format if not specified
569-
format = infer_format(path, format)
570-
571-
# Convert figures to dicts (and validate if requested)
572-
# TODO: Keep same iterable type
573-
fig_dicts = [validate_coerce_fig_to_dict(fig, validate) for fig in figs]
574-
575-
kaleido.write_fig_sync(
576-
fig_dicts,
577-
directory=path,
578-
opts=dict(
579-
format=format or defaults.default_format,
580-
width=width or defaults.default_width,
581-
height=height or defaults.default_height,
582-
scale=scale or defaults.default_scale,
583-
),
569+
# Broadcast arguments into correct format for passing to Kaleido
570+
arg_dicts = broadcast_args_to_dicts(
571+
fig=fig,
572+
file=file,
573+
format=format,
574+
scale=scale,
575+
width=width,
576+
height=height,
577+
validate=validate,
584578
)
585579

586-
# # Get individual arguments
587-
# individual_args, individual_kwargs = as_individual_args(*args, **kwargs)
588-
589-
# if kaleido_available() and kaleido_major() > 0:
590-
# # Kaleido v1
591-
# # TODO: Use a single shared kaleido instance for all images
592-
# for a, kw in zip(individual_args, individual_kwargs):
593-
# write_image(*a, **kw)
594-
# else:
595-
# # Kaleido v0, or orca
596-
# for a, kw in zip(individual_args, individual_kwargs):
597-
# write_image(*a, **kw)
580+
# For each dict:
581+
# - convert figures to dicts (and validate if requested)
582+
# - try to cast `file` as a Path object
583+
for d in arg_dicts:
584+
d["fig"] = validate_coerce_fig_to_dict(d["fig"], d["validate"])
585+
d["file"] = as_path_object(d["file"])
586+
587+
# Reshape arg_dicts into correct format for passing to Kaleido
588+
# We call infer_format() here rather than above so that the `file` argument
589+
# has already been cast to a Path object.
590+
# Also insert defaults for any missing arguments as needed
591+
kaleido_specs = [
592+
{
593+
"fig": d["fig"],
594+
"path": d["file"],
595+
"opts": dict(
596+
format=infer_format(d["file"], d["format"]) or defaults.default_format,
597+
width=d["width"] or defaults.default_width,
598+
height=d["height"] or defaults.default_height,
599+
scale=d["scale"] or defaults.default_scale,
600+
),
601+
}
602+
for d in arg_dicts
603+
]
604+
605+
import choreographer
606+
607+
try:
608+
kaleido.write_fig_from_object_sync(kaleido_specs)
609+
except choreographer.errors.ChromeNotFoundError:
610+
raise RuntimeError(PLOTLY_GET_CHROME_ERROR_MSG)
598611

599612

600613
def full_figure_for_development(fig, warn=True, as_dict=False):

plotly/io/_utils.py

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -43,55 +43,47 @@ def validate_coerce_output_type(output_type):
4343
return cls
4444

4545

46-
def as_individual_args(*args, **kwargs):
46+
def broadcast_args_to_dicts(**kwargs):
4747
"""
48-
Given one or more positional or keyword arguments which may be either a single value
49-
or a list of values, return a list of lists and a list of dictionaries
50-
by expanding the single values into lists.
48+
Given one or more keyword arguments which may be either a single value or a list of values,
49+
return a list of keyword dictionaries by broadcasting the single valuesacross all the dicts.
5150
If more than one item in the input is a list, all lists must be the same length.
5251
5352
Parameters
5453
----------
55-
*args: list
56-
The positional arguments
5754
**kwargs: dict
5855
The keyword arguments
5956
6057
Returns
6158
-------
62-
list of lists
63-
A list of lists
6459
list of dicts
6560
A list of dictionaries
61+
62+
Raises
63+
------
64+
ValueError
65+
If any of the input lists are not the same length
6666
"""
6767
# Check that all list arguments have the same length,
6868
# and find out what that length is
6969
# If there are no list arguments, length is 1
70-
list_lengths = [
71-
len(v) for v in args + tuple(kwargs.values()) if isinstance(v, list)
72-
]
70+
list_lengths = [len(v) for v in tuple(kwargs.values()) if isinstance(v, list)]
7371
if list_lengths and len(set(list_lengths)) > 1:
7472
raise ValueError("All list arguments must have the same length.")
7573
list_length = list_lengths[0] if list_lengths else 1
7674

7775
# Expand all arguments to lists of the same length
78-
expanded_args = [[v] * list_length if not isinstance(v, list) else v for v in args]
7976
expanded_kwargs = {
8077
k: [v] * list_length if not isinstance(v, list) else v
8178
for k, v in kwargs.items()
8279
}
83-
84-
# Reshape into a list of lists
85-
# Each list represents the positional arguments for a single function call
86-
list_of_args = [[v[i] for v in expanded_args] for i in range(list_length)]
87-
8880
# Reshape into a list of dictionaries
8981
# Each dictionary represents the keyword arguments for a single function call
9082
list_of_kwargs = [
9183
{k: v[i] for k, v in expanded_kwargs.items()} for i in range(list_length)
9284
]
9385

94-
return list_of_args, list_of_kwargs
86+
return list_of_kwargs
9587

9688

9789
def plotly_cdn_url(cdn_ver=get_plotlyjs_version()):

0 commit comments

Comments
 (0)