Skip to content

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

Open
ahmojo wants to merge 2 commits 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 2 commits into
plotly:mainfrom
ahmojo:fix-5615-splom-colorbar-restyle

Conversation

@ahmojo

@ahmojo ahmojo commented Jul 5, 2026

Copy link
Copy Markdown

Link to issue

Related to #5615

Description of change

Setting a nested property to None after it had been set could leave an empty compound container in the emitted figure JSON. For example, clearing marker.colorbar.thicknessmode left marker: {"colorbar": {}}. The same residue could be created when plotly_restyle(..., None) targeted a never-set nested path, even though no restyle message was sent.

This is inconsistent with construction, where empty compound containers are already pruned. This PR makes removal of a missing path a no-op and prunes emptied scalar compound containers while preserving list positions.

In the static-HTML reproduction from #5615, removing that residue prevents it from being included in the initial payload and avoids the subsequent JavaScript restyle collapse. This does not change plotly.js itself: a live FigureWidget whose browser-side state already contains colorbar: {} can still collapse after a later restyle.

Demo

I isolated the Python serialization trigger using the issue's Playwright reproduction (1200x800 viewport, the same bundled plotly.js for every variant):

Variant Initial payload Plot width after JS restyle Colorbar thickness
Pristine figure no colorbar key 543px 30px
Residual container from main {"colorbar": {}} 64px 16290px
Same static-HTML repro with this PR no colorbar key 543px 30px

With this change, the static-HTML output matches the pristine control. The separate live-FigureWidget path remains a plotly.js/frontend-state issue and is outside the scope of this cleanup.

Testing strategy

Added two regression tests in tests/test_core/test_figure_messages/test_plotly_restyle.py:

  • Removing a never-set nested path leaves the figure unchanged and sends no restyle message.
  • Clearing a nested property removes the empty scalar compound ancestors from emitted JSON.

Targeted checks:

  • uv run --with-editable . --with pytest python -m pytest tests/test_core/test_figure_messages/test_plotly_restyle.py - 10 passed
  • uv run --with-editable . --with pytest python -m pytest tests/test_core/test_graph_objs/test_property_assignment.py - 28 passed
  • uv run --with ruff ruff check plotly/basedatatypes.py tests/test_core/test_figure_messages/test_plotly_restyle.py - passed

Additional information (optional)

This changes serialized output for set-then-unset scalar compound properties across the library, bringing assignment and restyle cleanup in line with construction-time behavior. Compound-array/list entries remain in place as positional placeholders, and batch updates are pruned when the batch flushes.

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
@camdecoster

Copy link
Copy Markdown
Contributor

Thanks for the PR! We'll take a look and follow up. We're a bit backed up at the moment, so it might take a while to get through it.

@emilykl

emilykl commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Hi @ahmojo, thanks for the contribution!

This is an interesting change. Pruning empty dicts left behind after a value is removed by setting it to None in plotly_restyle() does seem to make sense at first glance, although I'm not sure what the practical effects are beyond possibly this particular bug.

The reproduction example in #5637 never actually calls plotly_restyle() at all, since it uses the JavaScript Plotly.restyle() function, so I don't quite see how this change would affect the result of that particular example. Were you able to reproduce the issue in pure Python using plotly_restyle()?

@ahmojo

ahmojo commented Jul 30, 2026

Copy link
Copy Markdown
Author

Hi @ahmojo, thanks for the contribution!

This is an interesting change. Pruning empty dicts left behind after a value is removed by setting it to None in plotly_restyle() does seem to make sense at first glance, although I'm not sure what the practical effects are beyond possibly this particular bug.

The reproduction example in #5637 never actually calls plotly_restyle() at all, since it uses the JavaScript Plotly.restyle() function, so I don't quite see how this change would affect the result of that particular example. Were you able to reproduce the issue in pure Python using plotly_restyle()?

Thanks for pointing this out @emilykl. I tested this with a live FigureWidget using only Python. A direct restyle on a pristine widget works normally, but the same set-and-clear history from the issue reproduces the collapse:

widget = go.FigureWidget(make_fig())
display(widget)

Then, after it renders:

widget.data[1].marker.colorbar.thicknessmode = "pixels"
widget.data[1].marker.colorbar.thicknessmode = None

widget.plotly_restyle(
    {"marker.colorbar.thicknessmode": "pixels"},
    trace_indexes=[1],
)

The clear leaves colorbar: {} in the browser-side trace. The final restyle changes the thickness to 16,290px and shrinks the plot width from 543px to 64px.
This test also showed that my PR fixes the original static-HTML repro, but not this live FigureWidget path. I’ll adjust the description so it doesn’t overstate the scope or claim to fully close #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.

3 participants