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 @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).

### Fixed
- Raise a clear `ValueError` when an unsupported marginal plot type is passed to Plotly Express, instead of failing later with a cryptic `'NoneType' object has no attribute 'constructor'` message [[#5625](https://github.com/plotly/plotly.py/pull/5625)], with thanks to @eugen-goebel for the contribution!
- Stop emitting leftover empty `{}` containers when a nested property is set to `None`, which could make a later `Plotly.restyle` of `marker.colorbar` attributes collapse the scatter-matrix layout [[#5615](https://github.com/plotly/plotly.py/issues/5615)]


## [6.8.0] - 2026-06-03
Expand Down
51 changes: 51 additions & 0 deletions plotly/basedatatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1904,21 +1904,32 @@ def _set_in(d, key_path_str, v):
# This variable will be assigned to the parent of the next key path
# element currently being processed
val_parent = d
parents = []

# Initialize parent dict or list of value to be assigned
# -----------------------------------------------------
for kp, key_path_el in enumerate(key_path[:-1]):
# Extend val_parent list if needed
if isinstance(val_parent, list) and isinstance(key_path_el, int):
if v is None and not 0 <= key_path_el < len(val_parent):
return False

while len(val_parent) <= key_path_el:
val_parent.append(None)

elif isinstance(val_parent, dict) and key_path_el not in val_parent:
if v is None:
return False

if isinstance(key_path[kp + 1], int):
val_parent[key_path_el] = []
else:
val_parent[key_path_el] = {}

elif not isinstance(val_parent, (dict, list)) and v is None:
return False

parents.append((val_parent, key_path_el))
val_parent = val_parent[key_path_el]

# Assign value to final parent dict or list
Expand All @@ -1945,6 +1956,16 @@ def _set_in(d, key_path_str, v):
# we can pop the key, which alters parent
val_parent.pop(last_key)
val_changed = True

for parent, key in reversed(parents):
child = parent[key]
if isinstance(child, dict) and not child:
if isinstance(parent, dict):
parent.pop(key)
else:
break
else:
break
elif isinstance(val_parent, list):
if isinstance(last_key, int) and 0 <= last_key < len(val_parent):
# Parent is a list and last_key is a valid index so we
Expand Down Expand Up @@ -4607,6 +4628,28 @@ def _init_child_props(self, child):
else:
raise ValueError("Invalid child with name: %s" % child.plotly_name)

def _prune_empty_child_props(self, child):
"""
Remove a compound child's properties dict if it is empty.

Compound array elements can rely on empty dict placeholders for index
position, so this only prunes scalar compound properties.
"""
if (
child.plotly_name in self._compound_props
and self._compound_props[child.plotly_name] is child
and self._props is not None
and self._props.get(child.plotly_name) == {}
):
self._props.pop(child.plotly_name)

if (
not self._props
and self.parent is not None
and isinstance(self.parent, BasePlotlyType)
):
self.parent._prune_empty_child_props(self)

def _get_child_prop_defaults(self, child):
"""
Return default properties dict for child
Expand Down Expand Up @@ -5287,6 +5330,14 @@ def _set_prop(self, prop, val):
# Send property update message
self._send_prop_set(prop, val)

if (
not self._in_batch_mode
and not self._props
and self.parent is not None
and isinstance(self.parent, BasePlotlyType)
):
self.parent._prune_empty_child_props(self)

# val is valid value
# ------------------
else:
Expand Down
31 changes: 31 additions & 0 deletions tests/test_core/test_figure_messages/test_plotly_restyle.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,34 @@ def test_plotly_restyle_multi_trace(self):
self.figure._send_restyle_msg.assert_called_once_with(
{"marker": {"color": "green"}, "name": "MARKER 1"}, trace_indexes=[0, 1]
)

def test_plotly_restyle_nested_none_does_not_create_empty_parents(self):
figure = go.Figure(data=[go.Splom()])
figure._send_restyle_msg = MagicMock()
expected = figure.to_plotly_json()

figure.plotly_restyle({"marker.colorbar.thicknessmode": None}, trace_indexes=0)

assert figure.to_plotly_json() == expected
figure._send_restyle_msg.assert_not_called()

def test_property_assignment_nested_none_prunes_empty_parents(self):
figure = go.Figure(
data=[
go.Splom(
dimensions=[{"values": [1, 2]}, {"values": [3, 4]}],
marker={
"color": [1, 2],
"colorbar": {"thicknessmode": "pixels"},
},
)
]
)
figure._send_restyle_msg = MagicMock()

figure.data[0].marker.colorbar.thicknessmode = None

assert figure.to_plotly_json()["data"][0]["marker"] == {"color": [1, 2]}
figure._send_restyle_msg.assert_called_once_with(
{"marker.colorbar.thicknessmode": [None]}, trace_indexes=0
)