Skip to content

Commit 088550e

Browse files
breken-aiclaude
andcommitted
Fix px.imshow placing the next animation frame in empty facet_col_wrap cells
With animation_frame and facet_col_wrap, fig.data kept every trace whose index was below nrows * ncols. When the facets do not fill the wrapped grid, the first facet of the second animation frame was drawn in the empty cell and stayed there during the animation. Only the first frame's facets are added to fig.data now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent d586d22 commit 088550e

3 files changed

Lines changed: 24 additions & 1 deletion

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
77
### Fixed
88
- 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!
99
- 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!
10+
- Fix `px.imshow` with `animation_frame` and `facet_col_wrap` drawing the first facet of the second animation frame in the empty cell of an incomplete facet grid, where it stayed while the animation played, with thanks to @breken-ai for the contribution!
1011

1112

1213
## [7.1.0] - 2026-09-15

‎plotly/express/_imshow.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -611,7 +611,10 @@ def imshow(
611611
nslices_facets = nslices_facet_row * nslices_facet_col
612612
frame_list = []
613613
for index, trace in enumerate(traces):
614-
if ((facet_col or facet_row) and index < nrows * ncols) or index == 0:
614+
# Only the first animation frame goes into fig.data. With facet_col_wrap,
615+
# nrows * ncols can exceed the number of facets, so compare with
616+
# nslices_facets to leave the unused cells of the grid empty.
617+
if ((facet_col or facet_row) and index < nslices_facets) or index == 0:
615618
# Calculate row and col position
616619
# index is ordered by (facet_row, facet_col) from itertools.product
617620
# When facet_col_wrap is used (and facet_row is None), traces are laid out

‎tests/test_optional/test_px/test_imshow.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,25 @@ def test_animation_and_facet(binary_string):
469469
assert len(fig.data) == img.shape[1]
470470

471471

472+
@pytest.mark.parametrize("binary_string", [False, True])
473+
def test_animation_and_facet_col_wrap_incomplete_grid(binary_string):
474+
# 5 facets wrapped in 2 columns leave the 6th cell of the 3x2 grid empty.
475+
# That cell must stay empty, instead of showing the first facet of the
476+
# second animation frame.
477+
img = np.arange(2 * 5 * 3 * 4).reshape(2, 5, 3, 4).astype(np.uint8)
478+
fig = px.imshow(
479+
img,
480+
animation_frame=0,
481+
facet_col=1,
482+
facet_col_wrap=2,
483+
binary_string=binary_string,
484+
)
485+
nslices_facet = img.shape[1]
486+
assert len(fig.frames) == img.shape[0]
487+
assert len(fig.data) == nslices_facet
488+
assert [trace.name for trace in fig.data] == [str(i) for i in range(5)]
489+
490+
472491
@pytest.mark.parametrize("facet_row", [0, 1, 2, -1])
473492
@pytest.mark.parametrize("binary_string", [False, True])
474493
def test_facet_row(facet_row, binary_string):

0 commit comments

Comments
 (0)