Skip to content

Fix add_annotation and add_shape slowing down cubically as a figure fills up - #5779

Open
jayzhou2309 wants to merge 2 commits into
plotly:mainfrom
jayzhou2309:fix/annotation-append-quadratic
Open

jayzhou2309 wants to merge 2 commits into
plotly:mainfrom
jayzhou2309:fix/annotation-append-quadratic

Conversation

@jayzhou2309

Copy link
Copy Markdown

Link to issue

Closes #5316

Description of change

add_annotation, add_shape, add_layout_image and add_selection append with self.layout[prop] += (new_obj,), which rebuilds the whole array. CompoundArrayValidator.validate_coerce copies every existing element, and each copy reads its own props through _get_child_props, which finds the element's index with an O(N) identity scan (_index_is). So one append costs O(N^2) and adding N objects costs O(N^3).

This PR adds BasePlotlyType._append_array_prop, which validates only the new element, appends its props, sends the same relayout message as before (the full array), and keeps the existing element objects. _add_annotation_like uses it. Under batch_update() it falls back to the old += path, so batch behavior is unchanged.

This is the focused version of the annotation part of #5520, which was closed because it bundled three changes.

Demo

Adding N annotations to an empty go.Figure() with fig.add_annotation(x=i, y=i, text=str(i)), Python 3.12 on macOS arm64:

N main (d586d22) this PR
250 0.90 s 0.014 s
500 4.34 s 0.026 s
1000 25.2 s 0.053 s

Testing strategy

  • test_add_annotation_keeps_existing_annotations checks that an earlier annotation object stays attached to the figure after another is added, and that editing it shows up in to_dict(). It fails on main (the object gets replaced by a copy) and passes here.
  • test_add_shape_sends_full_array_relayout pins the relayout message to the full shapes array. It passes on main and on this branch.
  • tests/test_core and tests/test_optional/test_autoshapes: 498 passed, 1 skipped. tests/test_optional/{test_subplots,test_graph_objs,test_px,test_figure_factory}: 1254 passed, 10 skipped, 4 xfailed.
  • fig.to_dict()["layout"] after an add inside batch_update(), add_vline and add_hrect with an annotation is identical on main and on this branch.
  • ruff check and ruff format --check pass on the changed files.

Additional information (optional)

One visible difference: a reference to an existing annotation (a = fig.layout.annotations[0]) now stays live after another annotation is added, instead of silently detaching. The new test pins that.

#5758 touches the same += line to handle batch mode. The two changes compose. If #5758 lands first, the batch-mode fallback here can go.

AI disclosure

An AI agent (Claude Code) reproduced the slowdown, profiled it with cProfile, wrote the failing test and the fix, and ran the tests, ruff, and the benchmark above.

Guidelines


🤖 Written and posted by an AI agent (Claude Code) on behalf of @jayzhou2309.

jayzhou2309 and others added 2 commits September 29, 2026 01:37
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>

This branch has not been deployed

No deployments
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.

Adding N annotations takes O(N^2) time

2 participants