Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- Fix `mpl_to_plotly` conversion of matplotlib contour lines and line collections: close contour rings ending with `Z` codes, support dash styles, convert date x-axes, separate disjoint subpaths with `None` separators, hide line collection traces from the legend by default, and group consecutive same-style lines into single traces [[#5770](https://github.com/plotly/plotly.py/pull/5770)], with thanks to @robertoffmoura 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!
- Fix color utilities mutating the list of colors passed to them [[#5836](https://github.com/plotly/plotly.py/pull/5836)], with thanks to @Enesuygurs for the contribution!
- Fix `go.Image` rejecting 3-element `zmin`/`zmax` values for 3-component colormodels such as `rgb` and `hsl`; the image trace's `zmin`/`zmax` now accept up to 4 elements so their length can match the colormodel [[#1973](https://github.com/plotly/plotly.py/issues/1973)]


## [7.1.0] - 2026-09-15
Expand Down
13 changes: 13 additions & 0 deletions codegen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ def preprocess_schema(plotly_schema):
if "concentrationscales" in items:
items["colorscale"] = items.pop("concentrationscales")

# The number of color components in the image trace's zmin/zmax
# depends on the colormodel (3 for rgb/hsl, 4 for rgba/rgba256/hsla),
# as described in the attributes' own descriptions. Validators are
# stateless and cannot see the colormodel of the trace being
# validated, so the fixed 4-element length from the plotly.js schema
# rejects valid 3-element values. Mark both properties as freeLength
# (accept up to 4 elements), as the schema already does for e.g.
# updatemenu button args and parcoords constraintrange.
# See https://github.com/plotly/plotly.py/issues/1973
image_attributes = plotly_schema["traces"]["image"]["attributes"]
for prop in ("zmin", "zmax"):
image_attributes[prop]["freeLength"] = True


def make_paths(outdir):
"""Make various paths needed for formatting and linting."""
Expand Down
4 changes: 2 additions & 2 deletions plotly/graph_objs/_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ def zmax(self):

The 'zmax' property is an info array that may be specified as:

* a list or tuple of 4 elements where:
* a list or tuple of up to 4 elements where:
(0) The 'zmax[0]' property is a number and may be specified as:
- An int or float
(1) The 'zmax[1]' property is a number and may be specified as:
Expand Down Expand Up @@ -682,7 +682,7 @@ def zmin(self):

The 'zmin' property is an info array that may be specified as:

* a list or tuple of 4 elements where:
* a list or tuple of up to 4 elements where:
(0) The 'zmin[0]' property is a number and may be specified as:
- An int or float
(1) The 'zmin[1]' property is a number and may be specified as:
Expand Down
2 changes: 2 additions & 0 deletions plotly/validators/_validators.json
Original file line number Diff line number Diff line change
Expand Up @@ -77687,6 +77687,7 @@
"plotly_name": "zmin",
"parent_name": "image",
"edit_type": "calc",
"free_length": true,
"items": [
{
"editType": "calc",
Expand All @@ -77713,6 +77714,7 @@
"plotly_name": "zmax",
"parent_name": "image",
"edit_type": "calc",
"free_length": true,
"items": [
{
"editType": "calc",
Expand Down
71 changes: 71 additions & 0 deletions tests/test_core/test_graph_objs/test_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import numpy as np
import pytest

import plotly.graph_objects as go

Z3 = np.random.randint(256, size=(10, 10, 3), dtype=np.uint8)
Z4 = np.random.randint(256, size=(10, 10, 4), dtype=np.uint8)


# Regression tests for https://github.com/plotly/plotly.py/issues/1973
# The number of components of zmin/zmax depends on the colormodel
# (3 for rgb/hsl, 4 for rgba/rgba256/hsla), so both lengths must validate.
@pytest.mark.parametrize(
"colormodel,z,zmin,zmax",
[
("rgb", Z3, [0, 0, 0], [255, 255, 255]),
("hsl", Z3, [0, 0, 0], [360, 100, 100]),
("rgba", Z4, [0, 0, 0, 0], [255, 255, 255, 1]),
("rgba256", Z4, [0, 0, 0, 0], [255, 255, 255, 255]),
("hsla", Z4, [0, 0, 0, 0], [360, 100, 100, 1]),
# A 4-element value must keep working for 3-component models too,
# as it did before this fix (it was the only length accepted).
("rgb", Z3, [0, 0, 0, 0], [255, 255, 255, 255]),
],
)
def test_image_zmin_zmax_matching_colormodel(colormodel, z, zmin, zmax):
img = go.Image(z=z, colormodel=colormodel, zmin=zmin, zmax=zmax)
assert img.zmin == tuple(zmin)
assert img.zmax == tuple(zmax)


def test_image_zmin_zmax_issue_example():
# The exact example from #1973
im = np.random.randint(256, size=(50, 50, 3), dtype=np.uint8)
fig = go.Figure(
go.Image(z=im, colormodel="rgb", zmin=[10, 10, 10], zmax=[100, 100, 100])
)
assert fig.data[0].zmin == (10, 10, 10)
assert fig.data[0].zmax == (100, 100, 100)


def test_image_zmin_zmax_default_colormodel():
# colormodel defaults to "rgb", so 3 elements are valid without it.
img = go.Image(z=Z3, zmin=[0, 0, 0], zmax=[255, 255, 255])
assert img.zmax == (255, 255, 255)


def test_image_zmin_zmax_assignment_order():
# Validation must not depend on the order properties are set in.
img = go.Image(z=Z3, zmin=[0, 0, 0, 0], zmax=[255, 255, 255, 255])
img.colormodel = "rgb"
img.zmin = [0, 0, 0]
img.zmax = [255, 255, 255]
assert img.zmin == (0, 0, 0)


@pytest.mark.parametrize("colormodel", ["rgb", "rgba", "rgba256", "hsl", "hsla"])
def test_image_zmin_zmax_five_elements_rejected(colormodel):
with pytest.raises(ValueError):
go.Image(z=Z4, colormodel=colormodel, zmax=[1, 2, 3, 4, 5])


def test_image_zmin_zmax_non_numeric_rejected():
with pytest.raises(ValueError):
go.Image(z=Z3, colormodel="rgb", zmax=["a", "b", "c"])


def test_heatmap_zmin_zmax_scalar_unaffected():
hm = go.Heatmap(z=[[1, 2], [3, 4]], zmin=0, zmax=5)
assert hm.zmin == 0
assert hm.zmax == 5