Skip to content

Commit 5d7743b

Browse files
adarshsmclaude
andcommitted
Don't modify the annotation passed to add_vline and friends
axis_spanning_shape_annotation filled in the annotation it was given (text from annotation_* kwargs, and x/y/xanchor/yanchor from the shape) in place. When the same dict or go.layout.Annotation was passed to several add_vline/add_hline/add_vrect/add_hrect calls, the first call left x/y in it, and later calls only set positions that weren't set already, so every annotation ended up at the first shape. Work on a copy of the annotation instead. Closes #3547 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent c6ab9c7 commit 5d7743b

3 files changed

Lines changed: 31 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
1717
- Fix Plotly Express mutating lists passed to the `x` or `y` arguments in wide mode [[#5727](https://github.com/plotly/plotly.py/pull/5727)], with thanks to @cpruijsen for the contribution!
1818
- 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!
1919
- Fix `mpl_to_plotly` conversion of matplotlib contour lines and line collections: close contour rings ending with `Z` codes, support dash styles, convert date x-axes, separate disjoint subpaths with `None` separators, hide line collection traces from the legend by default, and group consecutive same-style lines into single traces [[#5770](https://github.com/plotly/plotly.py/pull/5770)], with thanks to @robertoffmoura for the contribution!
20+
- Fix `add_vline`, `add_hline`, `add_vrect` and `add_hrect` modifying the dict or `go.layout.Annotation` passed as `annotation`, which placed every later shape's annotation at the first shape's position when the same `annotation` was reused [[#3547](https://github.com/plotly/plotly.py/issues/3547)], with thanks to @adarshsm for the contribution!
2021

2122
## [7.1.0] - 2026-09-15
2223

‎plotly/shapeannotation.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# some functions defined here to avoid numpy import
22

3+
import copy
34
import datetime
45

56

@@ -244,6 +245,10 @@ def axis_spanning_shape_annotation(annotation, shape_type, shape_args, kwargs):
244245
# go.layout.Annotation ?
245246
if annotation is None:
246247
annotation = dict()
248+
else:
249+
# Fill in a copy, so that an annotation passed in can be reused for
250+
# other shapes without keeping this shape's text and position.
251+
annotation = copy.copy(annotation)
247252
for k in annotation_keys:
248253
if k == "annotation_position":
249254
# don't set so that Annotation constructor doesn't complain

‎tests/test_optional/test_autoshapes/test_annotated_shapes.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import plotly.graph_objects as go
2525
from plotly.subplots import make_subplots
2626
from itertools import product
27+
import copy
2728
import os
2829
import pytest
2930
import json
@@ -282,6 +283,30 @@ def test_specify_annotation_as_dict(multi_plot_fixture):
282283
assert ret
283284

284285

286+
# Test that the annotation passed in isn't modified, so it can be reused for
287+
# several shapes
288+
@pytest.mark.parametrize(
289+
"annotation",
290+
[dict(font_color="red"), go.layout.Annotation(font_color="red")],
291+
ids=["dict", "Annotation"],
292+
)
293+
def test_reuse_annotation(single_plot_fixture, annotation):
294+
before = copy.deepcopy(annotation)
295+
single_plot_fixture.add_vline(x=2, annotation_text="A", annotation=annotation)
296+
single_plot_fixture.add_vline(x=3, annotation_text="B", annotation=annotation)
297+
assert annotation == before
298+
ret = len(single_plot_fixture.layout.annotations) == 2
299+
for sh, d in zip(
300+
single_plot_fixture.layout.annotations,
301+
[
302+
{"text": "A", "x": 2, "font.color": "red"},
303+
{"text": "B", "x": 3, "font.color": "red"},
304+
],
305+
):
306+
ret &= _cmp_partial_dict(sh, d)
307+
assert ret
308+
309+
285310
# Test the default and coerced positions work
286311
def test_default_annotation_positions(multi_plot_fixture):
287312
# default position is (inside) top right

0 commit comments

Comments
 (0)