Skip to content

Commit 65135b5

Browse files
jayzhou2309claude
andcommitted
Append layout objects without rebuilding the existing array
add_annotation, add_shape, add_layout_image and add_selection appended with `self.layout[prop] += (new_obj,)`. That re-validated every existing element into a fresh copy, and each copy looked up its own props with an O(N) identity scan, so one append cost O(N^2) and N appends O(N^3). _append_array_prop validates only the new element and keeps the existing ones. Batch mode keeps the old path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 83a023c commit 65135b5

2 files changed

Lines changed: 34 additions & 1 deletion

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
88
- Fix concurrent first access to lazily initialized graph object properties, which could raise `ValueError("Invalid value")` [[#5691](https://github.com/plotly/plotly.py/pull/5691)], with thanks to @hb1915 for the contribution!
99
- Fix `mpl_to_plotly` crashing on touching bars (such as `plt.hist`) due to floating-point noise producing negative `bargap` values by clamping `bargap` to `[0, 1]` [[#5696](https://github.com/plotly/plotly.py/pull/5696)], with thanks to @robertoffmoura for the contribution!
1010
- 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 [[#5766](https://github.com/plotly/plotly.py/pull/5766)], with thanks to @Irahan2 for the contribution!
11+
- 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) [[#5779](https://github.com/plotly/plotly.py/pull/5779)]
1112

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

‎plotly/basedatatypes.py‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1627,7 +1627,7 @@ def _add_domain(ax_letter, new_axref):
16271627
xref, yref = map(lambda t: _add_domain(*t), zip(["x", "y"], [xref, yref]))
16281628
new_obj.update(xref=xref, yref=yref)
16291629

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

5372+
def _append_array_prop(self, prop, val):
5373+
"""
5374+
Append one element to a compound array property
5375+
5376+
Unlike `self[prop] += (val,)`, this validates only the new element
5377+
and keeps the existing elements, so N appends take O(N) time
5378+
instead of rebuilding every existing element on each append.
5379+
5380+
Parameters
5381+
----------
5382+
prop : str
5383+
Name of a compound array property
5384+
val
5385+
The element to append
5386+
"""
5387+
if self._in_batch_mode:
5388+
self[prop] += (val,)
5389+
return
5390+
5391+
curr_val = self[prop]
5392+
validator = self._get_validator(prop)
5393+
(new_el,) = validator.validate_coerce([val], skip_invalid=self._skip_invalid)
5394+
5395+
self._init_props()
5396+
new_dict_vals = self._props.get(prop, []) + [deepcopy(new_el._props)]
5397+
self._props[prop] = new_dict_vals
5398+
self._send_prop_set(prop, new_dict_vals)
5399+
5400+
new_el._orphan_props.clear()
5401+
new_el._parent = self
5402+
self._compound_array_props[prop] = list(curr_val) + [new_el]
5403+
53725404
def _send_prop_set(self, prop_path_str, val):
53735405
"""
53745406
Notify parent that a property has been set to a new value

0 commit comments

Comments
 (0)