From 627524ea4c0de4a46aacc41316ec222dc928bc27 Mon Sep 17 00:00:00 2001 From: Mohammad Hijjawi Date: Sun, 27 Sep 2026 15:02:52 +0100 Subject: [PATCH] Ignore NaN when inferring missing zmin/zmax in px.imshow When only one of zmin/zmax was passed (or binary_string was used) with contrast_rescaling="minmax", the missing bound was computed with img.min()/img.max(), which return NaN if the image contains NaN values. The resulting NaN cmin/cmax made the color range fall back to autoscaling, so the user-provided bound was ignored. Use np.nanmin/np.nanmax instead. Fixes #4299 --- CHANGELOG.md | 1 + plotly/express/_imshow.py | 4 ++-- tests/test_optional/test_px/test_imshow.py | 11 +++++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0583fec45c8..57e301cf7a7 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 `px.imshow` ignoring `zmin`/`zmax` when only one of them is passed and the image contains NaN values, since the missing bound was computed as NaN [[#4299](https://github.com/plotly/plotly.py/issues/4299)], with thanks to @MohammadHijjawi97 for the contribution! ## [7.1.0] - 2026-09-15 diff --git a/plotly/express/_imshow.py b/plotly/express/_imshow.py index 576b114d518..ca4c7214463 100644 --- a/plotly/express/_imshow.py +++ b/plotly/express/_imshow.py @@ -412,9 +412,9 @@ def imshow( if contrast_rescaling == "minmax": # When using binary_string and minmax we need to set zmin and zmax to rescale the image if (zmin is not None or binary_string) and zmax is None: - zmax = img.max() + zmax = np.nanmax(img) if (zmax is not None or binary_string) and zmin is None: - zmin = img.min() + zmin = np.nanmin(img) else: # For uint8 data and infer we let zmin and zmax to be None if passed as None if zmax is None and img.dtype != np.uint8: diff --git a/tests/test_optional/test_px/test_imshow.py b/tests/test_optional/test_px/test_imshow.py index 3a0e230eb9c..dc7522be850 100644 --- a/tests/test_optional/test_px/test_imshow.py +++ b/tests/test_optional/test_px/test_imshow.py @@ -183,6 +183,17 @@ def test_zmin_zmax_range_color(): assert fig.layout.coloraxis.cmax == 0.8 +def test_zmin_zmax_nan_data(): + # NaN values should be ignored when inferring the missing bound + img = np.array([[1.0, 2.0], [3.0, np_nan()]]) + fig = px.imshow(img, zmax=2) + assert fig.layout.coloraxis.cmin == 1.0 + assert fig.layout.coloraxis.cmax == 2.0 + fig = px.imshow(img, zmin=2) + assert fig.layout.coloraxis.cmin == 2.0 + assert fig.layout.coloraxis.cmax == 3.0 + + def test_zmin_zmax_range_color_source(): img = img_gray / 100.0 fig1 = px.imshow(img, zmin=0.2, zmax=0.8, binary_string=True)