Skip to content

Don't leave empty dict containers behind when nested properties are set to None#5637

Open
ahmojo wants to merge 1 commit into
plotly:mainfrom
ahmojo:fix-5615-splom-colorbar-restyle
Open

Don't leave empty dict containers behind when nested properties are set to None#5637
ahmojo wants to merge 1 commit into
plotly:mainfrom
ahmojo:fix-5615-splom-colorbar-restyle

Conversation

@ahmojo

@ahmojo ahmojo commented Jul 5, 2026

Copy link
Copy Markdown

Link to issue

Closes #5615

Description of change

Setting a nested property to None (e.g. fig.data[i].marker.colorbar.thicknessmode = None, or plotly_restyle({"marker.colorbar.thicknessmode": None})) left a residual empty {} container in the emitted figure JSON. For splom traces, that residual marker: {colorbar: {}} causes a later Plotly.restyle of colorbar attributes to run plotly.js's thicknessmode fraction→pixels conversion and write a huge thickness (30 × plot width, e.g. 16290px) into the trace, collapsing the scatter-matrix layout. This PR makes removal of a non-existent path a no-op and prunes emptied {} containers, so the figure JSON only contains properties the user actually set.

Demo

I isolated the trigger with the issue's Playwright repro (1200×800 viewport, bundled plotly.js):

Variant Initial payload Plot width after restyle colorbar thickness
Pristine figure + JS restyle no colorbar key 543px (normal) 30
Residual colorbar: {} + JS restyle (the issue) {"colorbar": {}} 64px (collapsed) 16290
Same, with this fix no colorbar key 543px (normal) 30

A pristine figure never collapses — only the residual empty dict emitted by plotly.py triggers it. With this fix, the issue's exact script renders normally.

Testing strategy

Added two regression tests in tests/test_core/test_figure_messages/test_plotly_restyle.py: one asserting that plotly_restyle(..., None) on a never-set path leaves the figure unchanged and sends no restyle message, and one asserting that unsetting a nested attribute leaves no empty {} residue in the emitted JSON. Both fail on main and pass with this change; tests/test_core passes.

Additional information (optional)

Lists are intentionally preserved as positional placeholders (e.g. dimensions[1] keeps its index), and pruning is skipped in batch mode until the batch flushes. Arguably plotly.js also shouldn't run the fraction→pixels thickness conversion for an empty pre-existing colorbar container when the default mode is already pixels — happy to file that upstream as a follow-up if useful.

Guidelines

…et to None

Setting a nested property such as marker.colorbar.thicknessmode to None
left a residual empty {} container in the figure's props, which was then
emitted in the figure JSON. For splom traces this residual
marker.colorbar {} made a later Plotly.restyle of colorbar attributes
misconvert the colorbar thickness and collapse the scatter-matrix layout.

_set_in now treats removal of a non-existent path as a no-op and prunes
emptied dict parents, and property assignment to None prunes emptied
compound-child dicts (lists are preserved as positional placeholders).

Fixes plotly#5615
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG]: marker.colorbar.thicknessmode='pixels' collapses scatter-matrix layout after Plotly.restyle, while initial render is normal

1 participant