From a926a30a89e7b7db6d8c33d9af121f310f66f911 Mon Sep 17 00:00:00 2001 From: Diksha Date: Thu, 2 Apr 2026 14:22:53 +0530 Subject: [PATCH 01/14] Fix Annotation arrow not updating after setting patchA Setting patchA on Annotation.arrow_patch has no visible effect because the arrow position is not recomputed. This change ensures that the annotation is marked as stale when patchA changes, triggering proper recomputation during rendering. --- lib/matplotlib/text.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 9c6478f9c7df..f06263f1a64b 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -2199,8 +2199,14 @@ def update_positions(self, renderer): xy=(bbox.x0 - pad / 2, bbox.y0 - pad / 2), width=bbox.width + pad, height=bbox.height + pad, transform=IdentityTransform(), clip_on=False) + old_patchA = self.arrow_patch.patchA self.arrow_patch.set_patchA(patchA) + # Ensure arrow updates when patchA changes + if old_patchA is not patchA: + self.stale = True + + @artist.allow_rasterization def draw(self, renderer): # docstring inherited From 92543f999f7504bac654ffca325f96fd87a9dd60 Mon Sep 17 00:00:00 2001 From: Diksha <228217662+dikshajangra12918-oss@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:17:53 +0530 Subject: [PATCH 02/14] Preserve user-set patchA in Annotation.update_positions Add logic to prevent overriding user-set patchA in update_positions. Fixes issues #28316. --- lib/matplotlib/text.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index f06263f1a64b..4be556d4d893 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -2199,14 +2199,18 @@ def update_positions(self, renderer): xy=(bbox.x0 - pad / 2, bbox.y0 - pad / 2), width=bbox.width + pad, height=bbox.height + pad, transform=IdentityTransform(), clip_on=False) - old_patchA = self.arrow_patch.patchA - self.arrow_patch.set_patchA(patchA) - - # Ensure arrow updates when patchA changes - if old_patchA is not patchA: - self.stale = True - + # Check if user manually set patchA externally + current_patchA = self.arrow_patch.patchA + internal_patchA = getattr(self, '_internal_patchA', None) + if current_patchA is not internal_patchA: + # patchA was manually set by the user, do not override it + pass + else: + # patchA was set internally, update it. + self.arrow_patch.set_patchA(patchA) + self._internal_patchA = patchA + @artist.allow_rasterization def draw(self, renderer): # docstring inherited From ac535a1d15c01f8ef3d1bd9f351572b9a43c74fa Mon Sep 17 00:00:00 2001 From: Diksha <228217662+dikshajangra12918-oss@users.noreply.github.com> Date: Sun, 5 Apr 2026 12:32:11 +0530 Subject: [PATCH 03/14] Add test_annotation_patchA_not_overridden function Add test to verify that manually set patchA on annotation arrow is preserved after draw. --- lib/matplotlib/tests/test_text.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index dad82f6af4fb..c8b9fccf35dc 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -1330,4 +1330,18 @@ def test_draw_text_as_path_fallback(monkeypatch): subfig = fig.subfigures(3, 1, height_ratios=heights) _test_complex_shaping(subfig[0]) _test_text_features(subfig[1]) - _test_text_language(subfig[2]) + _test_text_language(subfig[2]) +def test_annotation_patchA_not_overridden(): + # Test that manually setting patchA on annotation arrow is not overridden by update_positions. See GitHub issue #28316. + fig, ax = plt.subplots() + ann = ax.annotate( + '', + xy=(0.5, 0.6), + xytext=(0.2, 0.5), + arrowprops=dict(arrowstyle="->") + ) + text = ax.text(0.2, 0.5, 'Text', ha='center', va='center') + ann.arrow_patch.set_patchA(text) + fig.draw_without_rendering() + assert ann.arrow_patch.patchA is text + From ae84e017d309e65105aba7507c71ee4e1fe42b80 Mon Sep 17 00:00:00 2001 From: Diksha <228217662+dikshajangra12918-oss@users.noreply.github.com> Date: Sun, 5 Apr 2026 16:58:43 +0530 Subject: [PATCH 04/14] Preserve user-set patchA in update_positions Only update patchA internally if user has not manually set it. --- lib/matplotlib/text.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 4be556d4d893..924fada26da8 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -2199,17 +2199,10 @@ def update_positions(self, renderer): xy=(bbox.x0 - pad / 2, bbox.y0 - pad / 2), width=bbox.width + pad, height=bbox.height + pad, transform=IdentityTransform(), clip_on=False) - # Check if user manually set patchA externally - current_patchA = self.arrow_patch.patchA - internal_patchA = getattr(self, '_internal_patchA', None) - - if current_patchA is not internal_patchA: - # patchA was manually set by the user, do not override it - pass - else: - # patchA was set internally, update it. - self.arrow_patch.set_patchA(patchA) - self._internal_patchA = patchA + if self.arrow_patch.patchA is getattr( + self, '_internal_patchA', None): + self.arrow_patch.set_patchA(patchA) + self._internal_patchA = patchA @artist.allow_rasterization def draw(self, renderer): From c02fcb330e32dd457c5a39215888ff894c61051b Mon Sep 17 00:00:00 2001 From: apple Date: Tue, 7 Apr 2026 02:03:54 +0530 Subject: [PATCH 05/14] Fix indentation and Whitespace in update_positions --- lib/matplotlib/text.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 924fada26da8..4d104e51574c 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -2199,11 +2199,11 @@ def update_positions(self, renderer): xy=(bbox.x0 - pad / 2, bbox.y0 - pad / 2), width=bbox.width + pad, height=bbox.height + pad, transform=IdentityTransform(), clip_on=False) - if self.arrow_patch.patchA is getattr( + if self.arrow_patch.patchA is getattr( self, '_internal_patchA', None): - self.arrow_patch.set_patchA(patchA) - self._internal_patchA = patchA - + self.arrow_patch.set_patchA(patchA) + self._internal_patchA = patchA + @artist.allow_rasterization def draw(self, renderer): # docstring inherited From 6902c6fb024abfb31da619760a6f914762af507f Mon Sep 17 00:00:00 2001 From: apple Date: Sat, 11 Apr 2026 15:46:09 +0530 Subject: [PATCH 06/14] ENH: Add IndirectTransform for lazy coordinate resolution --- lib/matplotlib/transforms.py | 59 ++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index a279de0dfd8b..3887c5361114 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -2660,6 +2660,65 @@ def get_matrix(self): self._inverted = None self._invalid = 0 return self._mtx + class IndirectTransform(Transform): + """ + A transform that wraps a callable and resolves it lazily at draw time. + + This is useful when the actual transform depends on something not + known until draw time, like another artist's bounding box. + + Parameters + ---------- + func : callable + A callable that takes no arguments and returns a `.Transform` + or `.BboxBase`. + + Examples + -------- + :: + + tr = IndirectTransform( + lambda: BboxTransformTo(some_artist.get_window_extent()) + ) + """ + + input_dims = 2 + output_dims = 2 + + def __init__(self, func, **kwargs): + if not callable(func): + raise TypeError( + f"func must be callable, not {type(func).__name__!r}" + ) + super().__init__(**kwargs) + self._func = func + + def _resolve(self): + """Call the wrapped function and return a proper Transform.""" + result = self._func() + if isinstance(result, BboxBase): + return BboxTransformTo(result) + elif isinstance(result, Transform): + return result + raise TypeError( + f"func must return a Transform or BboxBase, " + f"not {type(result).__name__!r}" + ) + + def get_affine(self): + return self._resolve().get_affine() + + def frozen(self): + return self._resolve().frozen() + + def inverted(self): + return self._resolve().inverted() + + def transform_affine(self, points): + return self._resolve().transform_affine(points) + + def transform_non_affine(self, points): + return self._resolve().transform_non_affine(points) class BboxTransformFrom(Affine2DBase): From 9ad21d5739600de89480117eeafe92e5c4450043 Mon Sep 17 00:00:00 2001 From: apple Date: Sat, 11 Apr 2026 17:08:07 +0530 Subject: [PATCH 07/14] Fix blank lines in IndirectTransform --- lib/matplotlib/transforms.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 3887c5361114..4d8181fbaffd 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -2660,7 +2660,9 @@ def get_matrix(self): self._inverted = None self._invalid = 0 return self._mtx - class IndirectTransform(Transform): + + +class IndirectTransform(Transform): """ A transform that wraps a callable and resolves it lazily at draw time. From 0adf520277692423d3359426190e3a3f76b061ac Mon Sep 17 00:00:00 2001 From: apple Date: Sat, 11 Apr 2026 17:13:01 +0530 Subject: [PATCH 08/14] Fix linting errors --- lib/matplotlib/transforms.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 4d8181fbaffd..cedf451005f0 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -2660,8 +2660,8 @@ def get_matrix(self): self._inverted = None self._invalid = 0 return self._mtx - - + + class IndirectTransform(Transform): """ A transform that wraps a callable and resolves it lazily at draw time. From ed8416596ffa7f4a716af36dd77d0ffe96123278 Mon Sep 17 00:00:00 2001 From: apple Date: Wed, 15 Apr 2026 14:57:03 +0530 Subject: [PATCH 09/14] Fix linting errors in test_text.py --- lib/matplotlib/tests/test_text.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index c8b9fccf35dc..31fda79893ce 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -1330,9 +1330,12 @@ def test_draw_text_as_path_fallback(monkeypatch): subfig = fig.subfigures(3, 1, height_ratios=heights) _test_complex_shaping(subfig[0]) _test_text_features(subfig[1]) - _test_text_language(subfig[2]) + _test_text_language(subfig[2]) + + def test_annotation_patchA_not_overridden(): - # Test that manually setting patchA on annotation arrow is not overridden by update_positions. See GitHub issue #28316. + # Test that manually setting patchA on annotation arrow is not + # overridden by update_positions. See GitHub issue #28316. fig, ax = plt.subplots() ann = ax.annotate( '', From aa3482582c7b1246e281e7ac3545c7d30db56605 Mon Sep 17 00:00:00 2001 From: apple Date: Wed, 15 Apr 2026 15:01:14 +0530 Subject: [PATCH 10/14] Remove trailing blank line in test_text.py --- lib/matplotlib/tests/test_text.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 31fda79893ce..11960b67037b 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -1347,4 +1347,3 @@ def test_annotation_patchA_not_overridden(): ann.arrow_patch.set_patchA(text) fig.draw_without_rendering() assert ann.arrow_patch.patchA is text - From f1fc85940397cdd2d35fb98872e80e4043a38d77 Mon Sep 17 00:00:00 2001 From: apple Date: Wed, 15 Apr 2026 15:18:18 +0530 Subject: [PATCH 11/14] Add IndirectTransform to transforms.pyi stub --- lib/matplotlib/transforms.pyi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/matplotlib/transforms.pyi b/lib/matplotlib/transforms.pyi index ebee3954a3a7..fe982e605f93 100644 --- a/lib/matplotlib/transforms.pyi +++ b/lib/matplotlib/transforms.pyi @@ -346,3 +346,7 @@ def offset_copy( class _ScaledRotation(Affine2DBase): def __init__(self, theta: float, trans_shift: Transform) -> None: ... def get_matrix(self) -> np.ndarray: ... + + +class IndirectTransform(Transform): + def __init__(self, func: Callable[..., Transform], **kwargs) -> None: ... From 202e4ff9b6755fa839a56b22317ed2e9ed5fb339 Mon Sep 17 00:00:00 2001 From: apple Date: Fri, 17 Apr 2026 04:00:17 +0530 Subject: [PATCH 12/14] Add Callable import to transforms.pyi --- lib/matplotlib/transforms.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/transforms.pyi b/lib/matplotlib/transforms.pyi index fe982e605f93..e2acee57d7ca 100644 --- a/lib/matplotlib/transforms.pyi +++ b/lib/matplotlib/transforms.pyi @@ -3,7 +3,7 @@ from .patches import Patch from .figure import Figure import numpy as np from numpy.typing import ArrayLike -from collections.abc import Iterable, Sequence +from collections.abc import Callable, Iterable, Sequence from typing import Literal DEBUG: bool @@ -349,4 +349,4 @@ class _ScaledRotation(Affine2DBase): class IndirectTransform(Transform): - def __init__(self, func: Callable[..., Transform], **kwargs) -> None: ... + def __init__(self, func: Callable[..., Transform], **kwargs) -> None: ... From ab1ec795dac9d5c4f9644e403aa4c27c7cff6530 Mon Sep 17 00:00:00 2001 From: apple Date: Fri, 17 Apr 2026 12:28:44 +0530 Subject: [PATCH 13/14] Added new line (transform_non_affine method) --- lib/matplotlib/transforms.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/transforms.pyi b/lib/matplotlib/transforms.pyi index e2acee57d7ca..371f9b7f4af1 100644 --- a/lib/matplotlib/transforms.pyi +++ b/lib/matplotlib/transforms.pyi @@ -350,3 +350,4 @@ class _ScaledRotation(Affine2DBase): class IndirectTransform(Transform): def __init__(self, func: Callable[..., Transform], **kwargs) -> None: ... + def transform_non_affine(self, points) -> np.ndarray: ... From a9ddebf02f06d839cdf008e29fd0abf76cd5fcef Mon Sep 17 00:00:00 2001 From: apple Date: Sat, 18 Apr 2026 04:09:15 +0530 Subject: [PATCH 14/14] Fix transform_affine stub parameter name --- lib/matplotlib/transforms.pyi | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/transforms.pyi b/lib/matplotlib/transforms.pyi index 371f9b7f4af1..4e2cb4682f87 100644 --- a/lib/matplotlib/transforms.pyi +++ b/lib/matplotlib/transforms.pyi @@ -351,3 +351,4 @@ class _ScaledRotation(Affine2DBase): class IndirectTransform(Transform): def __init__(self, func: Callable[..., Transform], **kwargs) -> None: ... def transform_non_affine(self, points) -> np.ndarray: ... + def transform_affine(self, points) -> np.ndarray: ...