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 @@ -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 `add_annotation`, `add_shape`, `add_layout_image` and `add_selection` rebuilding every existing layout object on each call, which made adding N objects take O(N^3) time (1000 annotations: 25 s → 0.05 s) [[#5316](https://github.com/plotly/plotly.py/issues/5316)]


## [7.1.0] - 2026-09-15
Expand Down
34 changes: 33 additions & 1 deletion plotly/basedatatypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1627,7 +1627,7 @@ def _add_domain(ax_letter, new_axref):
xref, yref = map(lambda t: _add_domain(*t), zip(["x", "y"], [xref, yref]))
new_obj.update(xref=xref, yref=yref)

self.layout[prop_plural] += (new_obj,)
self.layout._append_array_prop(prop_plural, new_obj)
# The 'new_obj.xref' and 'new_obj.yref' parameters need to be reset otherwise it
# will appear as if user supplied yref params when looping through subplots and
# will force annotation to be on the axis of the last drawn annotation
Expand Down Expand Up @@ -5369,6 +5369,38 @@ def _set_array_prop(self, prop, val):
self._compound_array_props[prop] = val
return val

def _append_array_prop(self, prop, val):
"""
Append one element to a compound array property

Unlike `self[prop] += (val,)`, this validates only the new element
and keeps the existing elements, so N appends take O(N) time
instead of rebuilding every existing element on each append.

Parameters
----------
prop : str
Name of a compound array property
val
The element to append
"""
if self._in_batch_mode:
self[prop] += (val,)
return

curr_val = self[prop]
validator = self._get_validator(prop)
(new_el,) = validator.validate_coerce([val], skip_invalid=self._skip_invalid)

self._init_props()
new_dict_vals = self._props.get(prop, []) + [deepcopy(new_el._props)]
self._props[prop] = new_dict_vals
self._send_prop_set(prop, new_dict_vals)

new_el._orphan_props.clear()
new_el._parent = self
self._compound_array_props[prop] = list(curr_val) + [new_el]

def _send_prop_set(self, prop_path_str, val):
"""
Notify parent that a property has been set to a new value
Expand Down
33 changes: 33 additions & 0 deletions tests/test_core/test_update_objects/test_update_annotations.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import types
from unittest import TestCase
from unittest.mock import MagicMock

import plotly.graph_objs as go
from plotly.subplots import make_subplots
Expand Down Expand Up @@ -428,3 +429,35 @@ def test_select_annotations_integer(select_annotations_integer):
assert (len(anns) == 1) and anns[0]["text"] == "B"
with pytest.raises(IndexError):
fig.select_annotations(row=2, col=2, selector=3)


def test_add_annotation_keeps_existing_annotations():
fig = go.Figure()
fig.add_annotation(text="first")
first = fig.layout.annotations[0]

fig.add_annotation(text="second")

assert fig.layout.annotations[0] is first
first.text = "edited"
assert fig.to_dict()["layout"]["annotations"] == [
{"text": "edited"},
{"text": "second"},
]


def test_add_shape_sends_full_array_relayout():
fig = go.Figure()
fig.add_shape(type="line", x0=0, x1=1)
fig._send_relayout_msg = MagicMock()

fig.add_shape(type="rect", x0=2, x1=3)

fig._send_relayout_msg.assert_called_once_with(
{
"shapes": [
{"type": "line", "x0": 0, "x1": 1},
{"type": "rect", "x0": 2, "x1": 3},
]
}
)