diff --git a/CHANGELOG.md b/CHANGELOG.md index 0583fec45c8..a75ea0be7c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Fixed - Fix concurrent first access to lazily initialized graph object properties, which could raise `ValueError("Invalid value")` [[#3441](https://github.com/plotly/plotly.py/issues/3441)], with thanks to @hb1915 for the contribution! - Fix `px.sunburst`, `px.treemap` and `px.icicle` listing sectors in a different order on every run when `path` is used with a Polars DataFrame; sectors now follow their order of first appearance for all dataframe backends [[#5765](https://github.com/plotly/plotly.py/issues/5765)], with thanks to @Irahan2 for the contribution! +- Fix `plotly.io.write_images` ignoring `layout.width`/`layout.height` (and the template's) and exporting every figure at the default 700x500 size; it now uses the same width/height priority as `write_image` and `to_image` [[#5784](https://github.com/plotly/plotly.py/pull/5784)], with thanks to @breken-ai for the contribution! ## [7.1.0] - 2026-09-15 diff --git a/plotly/io/_kaleido.py b/plotly/io/_kaleido.py index 42adcf1e58e..17995aeb6f8 100644 --- a/plotly/io/_kaleido.py +++ b/plotly/io/_kaleido.py @@ -90,6 +90,31 @@ def infer_format(path: Union[Path, None], format: Union[str, None]) -> Union[str return format +def _resolve_width_height( + fig_dict: dict, width: Union[int, None], height: Union[int, None] +) -> tuple: + """ + Return the image width and height to request from Kaleido, in priority order: + the `width`/`height` arguments, then `layout.width`/`layout.height`, then the + template's `layout.width`/`layout.height`, then the plotly.io defaults. + """ + layout = fig_dict.get("layout", {}) + template_layout = layout.get("template", {}).get("layout", {}) + width = ( + width + or layout.get("width") + or template_layout.get("width") + or defaults.default_width + ) + height = ( + height + or layout.get("height") + or template_layout.get("height") + or defaults.default_height + ) + return width, height + + def to_image( fig: Union[dict, plotly.graph_objects.Figure], format: Union[str, None] = None, @@ -163,24 +188,7 @@ def to_image( if defaults.headers: kopts["headers"] = defaults.headers - width = ( - width - or fig_dict.get("layout", {}).get("width") - or fig_dict.get("layout", {}) - .get("template", {}) - .get("layout", {}) - .get("width") - or defaults.default_width - ) - height = ( - height - or fig_dict.get("layout", {}).get("height") - or fig_dict.get("layout", {}) - .get("template", {}) - .get("layout", {}) - .get("height") - or defaults.default_height - ) + width, height = _resolve_width_height(fig_dict, width, height) img_bytes = kaleido.calc_fig_sync( fig_dict, @@ -405,20 +413,23 @@ def write_images( # 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, + kaleido_specs = [] + for d in arg_dicts: + width, height = _resolve_width_height(d["fig"], d["width"], d["height"]) + kaleido_specs.append( + dict( + fig=d["fig"], + path=d["file"], + opts=dict( + format=infer_format(d["file"], d["format"]) + or defaults.default_format, + width=width, + height=height, + scale=d["scale"] or defaults.default_scale, + ), + topojson=defaults.topojson, + ) ) - for d in arg_dicts - ] from kaleido.errors import ChromeNotFoundError diff --git a/tests/test_optional/test_kaleido/test_kaleido.py b/tests/test_optional/test_kaleido/test_kaleido.py index d1573009e4a..921477c1e18 100644 --- a/tests/test_optional/test_kaleido/test_kaleido.py +++ b/tests/test_optional/test_kaleido/test_kaleido.py @@ -335,3 +335,24 @@ def test_width_height_priority(): assert height == pio.defaults.default_height, ( "Default height should be used when no layout or argument" ) + + +def test_write_images_width_height_priority(tmp_path): + """write_images uses the same width/height priority as to_image: + arguments > layout.width/height > defaults.""" + paths = [tmp_path / f"fig{i}.svg" for i in range(3)] + pio.write_images( + [ + create_figure(width=800, height=600), + create_figure(width=800, height=600), + create_figure(), + ], + paths, + width=[None, 1200, None], + ) + dimensions = [parse_svg_dimensions(path.read_bytes()) for path in paths] + assert dimensions[0] == (800, 600), "Layout dimensions should be used" + assert dimensions[1] == (1200, 600), ( + "Width argument should override layout, height should use layout" + ) + assert dimensions[2] == (pio.defaults.default_width, pio.defaults.default_height)