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
53 changes: 53 additions & 0 deletions doc/devel/examples/repro_annotate_xy_mutability.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Reproduction script for annotation xy mutability regression (Issue #72).

Run this module to observe how mutating the original ``xy`` or ``xytext``
arrays changes the rendered annotation position. On affected builds the
rendered position drifts to the mutated coordinates after the redraw.
"""

import matplotlib


matplotlib.use("Agg")

import matplotlib.pyplot as plt # noqa: E402
import numpy as np # noqa: E402


def main() -> None:
fig, ax = plt.subplots(layout="constrained")

xy = np.array([0.25, 0.75], dtype=float)
xytext = np.array([24.0, -12.0], dtype=float)

annotation = ax.annotate(
"Mutable source demo",
xy=xy,
xytext=xytext,
textcoords="offset points",
arrowprops=dict(arrowstyle="->"),
)

def snapshot(tag: str) -> None:
fig.canvas.draw()
renderer = fig.canvas.get_renderer()
xy_pixels = annotation._get_position_xy(renderer)
print(f"{tag} annotation xy (data): {annotation.xy}")
print(f"{tag} annotation xy in pixels: {xy_pixels}")
print(f"{tag} annotation xytext (offset points): {annotation.get_position()}")

snapshot("Initial")

xy[:] = [0.6, 0.2]
xytext[:] = [-18.0, 30.0]

snapshot("After mutation")

ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.set_title("Annotation mutability reproduction")
fig.savefig("repro_annotate_xy_mutability.png", dpi=150)


if __name__ == "__main__":
main()
79 changes: 78 additions & 1 deletion lib/matplotlib/tests/test_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import matplotlib.transforms as mtransforms
from matplotlib.testing.decorators import check_figures_equal, image_comparison
from matplotlib.testing._markers import needs_usetex
from matplotlib.text import Text, Annotation
from matplotlib.text import Text, Annotation, OffsetFrom

pyparsing_version = parse_version(pyparsing.__version__)

Expand Down Expand Up @@ -676,6 +676,83 @@ def test_annotation_units(fig_test, fig_ref):
ax.annotate("x", (datetime.now(), 0.5), xycoords=("data", "axes fraction"))


def test_annotation_xy_numpy_source_mutation_no_effect():
fig, ax = plt.subplots()
xy = np.array([0.25, 0.75], dtype=float)
ann = ax.annotate("point", xy=xy)

def current_xy_pixels():
fig.canvas.draw()
renderer = fig.canvas.get_renderer()
return ann._get_position_xy(renderer)

original_pixels = current_xy_pixels()
xy[:] = [0.5, 0.5]
mutated_pixels = current_xy_pixels()

np.testing.assert_allclose(mutated_pixels, original_pixels)
assert ann.xy == (0.25, 0.75)


def test_annotation_xytext_numpy_source_mutation_no_effect():
fig, ax = plt.subplots()
xytext = np.array([12.0, -6.0], dtype=float)
ann = ax.annotate(
"label",
xy=(0.4, 0.6),
xytext=xytext,
textcoords="offset points",
)

def current_text_position():
fig.canvas.draw()
return ann.get_position()

original_xytext = current_text_position()
xytext[:] = [18.0, -12.0]
mutated_xytext = current_text_position()

assert mutated_xytext == original_xytext
assert original_xytext == ann.get_position()


def test_annotation_xy_list_source_mutation_no_effect():
fig, ax = plt.subplots()
xy = [0.1, 0.3]
ann = ax.annotate("list", xy=xy)

def current_xy_pixels():
fig.canvas.draw()
renderer = fig.canvas.get_renderer()
return ann._get_position_xy(renderer)

original_pixels = current_xy_pixels()
xy[0] = 0.9
xy[1] = 0.9
mutated_pixels = current_xy_pixels()

np.testing.assert_allclose(mutated_pixels, original_pixels)
assert ann.xy == (0.1, 0.3)


def test_offsetfrom_ref_coord_numpy_mutation_no_effect():
fig, ax = plt.subplots()
ref_coord = np.array([0.25, 0.5], dtype=float)
offset = OffsetFrom(ax, ref_coord, unit="points")

fig.canvas.draw()
renderer = fig.canvas.get_renderer()
original_offset = offset(renderer).transform((0, 0))

ref_coord[:] = [0.75, 0.2]
fig.canvas.draw()
renderer = fig.canvas.get_renderer()
updated_offset = offset(renderer).transform((0, 0))

np.testing.assert_allclose(updated_offset, original_offset)
assert offset._ref_coord == (0.25, 0.5)


@image_comparison(['large_subscript_title.png'], style='mpl20')
def test_large_subscript_title():
# Remove this line when this test image is regenerated.
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -1389,7 +1389,7 @@ def __init__(self, artist, ref_coord, unit="points"):
The screen units to use (pixels or points) for the offset input.
"""
self._artist = artist
self._ref_coord = ref_coord
self._ref_coord = tuple(ref_coord)
self.set_unit(unit)

def set_unit(self, unit):
Expand Down Expand Up @@ -1456,7 +1456,7 @@ def __init__(self,
xycoords='data',
annotation_clip=None):

self.xy = xy
self.xy = tuple(xy)
self.xycoords = xycoords
self.set_annotation_clip(annotation_clip)

Expand Down