Skip to content

Commit 9641fff

Browse files
QuLogicjvcsir
authored andcommitted
Merge pull request #24143 from anntzer/cr
Add QuadContourSet.remove.
2 parents dad3ae3 + 67ed913 commit 9641fff

File tree

4 files changed

+270
-13
lines changed

4 files changed

+270
-13
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
"""
2+
===================================
3+
Angle annotations on bracket arrows
4+
===================================
5+
6+
This example shows how to add angle annotations to bracket arrow styles
7+
created using `.FancyArrowPatch`. Angles are annotated using
8+
``AngleAnnotation`` from the example
9+
:doc:`/gallery/text_labels_and_annotations/angle_annotation`.
10+
Additional `.FancyArrowPatch` arrows are added to show the directions of
11+
*angleA* and *angleB*.
12+
"""
13+
14+
#############################################################################
15+
# The ``AngleAnnotation`` class is copied from the
16+
# :doc:`/gallery/text_labels_and_annotations/angle_annotation` example.
17+
18+
import matplotlib.pyplot as plt
19+
import numpy as np
20+
from matplotlib.patches import Arc, FancyArrowPatch
21+
from matplotlib.transforms import Bbox, IdentityTransform, TransformedBbox
22+
23+
24+
class AngleAnnotation(Arc):
25+
"""
26+
Draws an arc between two vectors which appears circular in display space.
27+
"""
28+
def __init__(self, xy, p1, p2, size=75, unit="points", ax=None,
29+
text="", textposition="inside", text_kw=None, **kwargs):
30+
"""
31+
Parameters
32+
----------
33+
xy, p1, p2 : tuple or array of two floats
34+
Center position and two points. Angle annotation is drawn between
35+
the two vectors connecting *p1* and *p2* with *xy*, respectively.
36+
Units are data coordinates.
37+
38+
size : float
39+
Diameter of the angle annotation in units specified by *unit*.
40+
41+
unit : str
42+
One of the following strings to specify the unit of *size*:
43+
44+
* "pixels": pixels
45+
* "points": points, use points instead of pixels to not have a
46+
dependence on the DPI
47+
* "axes width", "axes height": relative units of Axes width, height
48+
* "axes min", "axes max": minimum or maximum of relative Axes
49+
width, height
50+
51+
ax : `matplotlib.axes.Axes`
52+
The Axes to add the angle annotation to.
53+
54+
text : str
55+
The text to mark the angle with.
56+
57+
textposition : {"inside", "outside", "edge"}
58+
Whether to show the text in- or outside the arc. "edge" can be used
59+
for custom positions anchored at the arc's edge.
60+
61+
text_kw : dict
62+
Dictionary of arguments passed to the Annotation.
63+
64+
**kwargs
65+
Further parameters are passed to `matplotlib.patches.Arc`. Use this
66+
to specify, color, linewidth etc. of the arc.
67+
68+
"""
69+
self.ax = ax or plt.gca()
70+
self._xydata = xy # in data coordinates
71+
self.vec1 = p1
72+
self.vec2 = p2
73+
self.size = size
74+
self.unit = unit
75+
self.textposition = textposition
76+
77+
super().__init__(self._xydata, size, size, angle=0.0,
78+
theta1=self.theta1, theta2=self.theta2, **kwargs)
79+
80+
self.set_transform(IdentityTransform())
81+
self.ax.add_patch(self)
82+
83+
self.kw = dict(ha="center", va="center",
84+
xycoords=IdentityTransform(),
85+
xytext=(0, 0), textcoords="offset points",
86+
annotation_clip=True)
87+
self.kw.update(text_kw or {})
88+
self.text = ax.annotate(text, xy=self._center, **self.kw)
89+
90+
def get_size(self):
91+
factor = 1.
92+
if self.unit == "points":
93+
factor = self.ax.figure.dpi / 72.
94+
elif self.unit[:4] == "axes":
95+
b = TransformedBbox(Bbox.unit(), self.ax.transAxes)
96+
dic = {"max": max(b.width, b.height),
97+
"min": min(b.width, b.height),
98+
"width": b.width, "height": b.height}
99+
factor = dic[self.unit[5:]]
100+
return self.size * factor
101+
102+
def set_size(self, size):
103+
self.size = size
104+
105+
def get_center_in_pixels(self):
106+
"""return center in pixels"""
107+
return self.ax.transData.transform(self._xydata)
108+
109+
def set_center(self, xy):
110+
"""set center in data coordinates"""
111+
self._xydata = xy
112+
113+
def get_theta(self, vec):
114+
vec_in_pixels = self.ax.transData.transform(vec) - self._center
115+
return np.rad2deg(np.arctan2(vec_in_pixels[1], vec_in_pixels[0]))
116+
117+
def get_theta1(self):
118+
return self.get_theta(self.vec1)
119+
120+
def get_theta2(self):
121+
return self.get_theta(self.vec2)
122+
123+
def set_theta(self, angle):
124+
pass
125+
126+
# Redefine attributes of the Arc to always give values in pixel space
127+
_center = property(get_center_in_pixels, set_center)
128+
theta1 = property(get_theta1, set_theta)
129+
theta2 = property(get_theta2, set_theta)
130+
width = property(get_size, set_size)
131+
height = property(get_size, set_size)
132+
133+
# The following two methods are needed to update the text position.
134+
def draw(self, renderer):
135+
self.update_text()
136+
super().draw(renderer)
137+
138+
def update_text(self):
139+
c = self._center
140+
s = self.get_size()
141+
angle_span = (self.theta2 - self.theta1) % 360
142+
angle = np.deg2rad(self.theta1 + angle_span / 2)
143+
r = s / 2
144+
if self.textposition == "inside":
145+
r = s / np.interp(angle_span, [60, 90, 135, 180],
146+
[3.3, 3.5, 3.8, 4])
147+
self.text.xy = c + r * np.array([np.cos(angle), np.sin(angle)])
148+
if self.textposition == "outside":
149+
def R90(a, r, w, h):
150+
if a < np.arctan(h/2/(r+w/2)):
151+
return np.sqrt((r+w/2)**2 + (np.tan(a)*(r+w/2))**2)
152+
else:
153+
c = np.sqrt((w/2)**2+(h/2)**2)
154+
T = np.arcsin(c * np.cos(np.pi/2 - a + np.arcsin(h/2/c))/r)
155+
xy = r * np.array([np.cos(a + T), np.sin(a + T)])
156+
xy += np.array([w/2, h/2])
157+
return np.sqrt(np.sum(xy**2))
158+
159+
def R(a, r, w, h):
160+
aa = (a % (np.pi/4))*((a % (np.pi/2)) <= np.pi/4) + \
161+
(np.pi/4 - (a % (np.pi/4)))*((a % (np.pi/2)) >= np.pi/4)
162+
return R90(aa, r, *[w, h][::int(np.sign(np.cos(2*a)))])
163+
164+
bbox = self.text.get_window_extent()
165+
X = R(angle, r, bbox.width, bbox.height)
166+
trans = self.ax.figure.dpi_scale_trans.inverted()
167+
offs = trans.transform(((X-s/2), 0))[0] * 72
168+
self.text.set_position([offs*np.cos(angle), offs*np.sin(angle)])
169+
170+
171+
#############################################################################
172+
# The plot is generatd with the following code.
173+
174+
def get_point_of_rotated_vertical(origin, line_length, degrees):
175+
"""Return xy coordinates of the vertical line end rotated by degrees."""
176+
rad = np.deg2rad(-degrees)
177+
return [origin[0] + line_length * np.sin(rad),
178+
origin[1] + line_length * np.cos(rad)]
179+
180+
181+
fig, ax = plt.subplots(figsize=(8, 7))
182+
ax.set(xlim=(0, 6), ylim=(-1, 4))
183+
ax.set_title('Angle annotations on bracket arrows')
184+
185+
for i, stylename in enumerate(["]-[", "|-|"]):
186+
for j, angle in enumerate([-40, 60]):
187+
y = 2*i + j
188+
arrow_centers = [(1, y), (5, y)]
189+
vlines = [[c[0], y + 0.5] for c in arrow_centers]
190+
widths = "widthA=1.5,widthB=1.5"
191+
arrowstyle = f"{stylename},{widths},angleA={angle},angleB={-angle}"
192+
patch = FancyArrowPatch(arrow_centers[0], arrow_centers[1],
193+
arrowstyle=arrowstyle, mutation_scale=25)
194+
ax.add_patch(patch)
195+
ax.text(3, y + 0.05, arrowstyle.replace(f"{widths},", ""),
196+
verticalalignment="bottom", horizontalalignment="center")
197+
ax.vlines([i[0] for i in vlines], [y, y], [i[1] for i in vlines],
198+
linestyles="--", color="C0")
199+
# Get the coordinates for the drawn patches at A and B
200+
patch_top_coords = [
201+
get_point_of_rotated_vertical(arrow_centers[0], 0.5, angle),
202+
get_point_of_rotated_vertical(arrow_centers[1], 0.5, -angle)
203+
]
204+
# Create points for annotating A and B with AngleAnnotation
205+
# Points include the top of the vline and patch_top_coords
206+
pointsA = [(1, y + 0.5), patch_top_coords[0]]
207+
pointsB = [patch_top_coords[1], (5, y + 0.5)]
208+
# Define the directions for the arrows for AngleAnnotation
209+
arrow_angles = [0.5, -0.5]
210+
# Reverse the points and arrow_angles when the angle is negative
211+
if angle < 0:
212+
pointsA.reverse()
213+
pointsB.reverse()
214+
arrow_angles.reverse()
215+
# Add AngleAnnotation and arrows to show angle directions
216+
data = zip(arrow_centers, [pointsA, pointsB], vlines, arrow_angles,
217+
patch_top_coords)
218+
for center, points, vline, arrow_angle, patch_top in data:
219+
am = AngleAnnotation(center, points[0], points[1], ax=ax,
220+
size=0.25, unit="axes min", text=str(-angle))
221+
arrowstyle = "Simple, tail_width=0.5, head_width=4, head_length=8"
222+
kw = dict(arrowstyle=arrowstyle, color="C0")
223+
arrow = FancyArrowPatch(vline, patch_top,
224+
connectionstyle=f"arc3,rad={arrow_angle}",
225+
**kw)
226+
ax.add_patch(arrow)
227+
228+
229+
#############################################################################
230+
#
231+
# .. admonition:: References
232+
#
233+
# The use of the following functions, methods, classes and modules is shown
234+
# in this example:
235+
#
236+
# - `matplotlib.patches.ArrowStyle`

lib/matplotlib/contour.py

+20-13
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
import matplotlib as mpl
1212
from matplotlib import _api, _docstring
1313
from matplotlib.backend_bases import MouseButton
14+
from matplotlib.text import Text
1415
import matplotlib.path as mpath
1516
import matplotlib.ticker as ticker
1617
import matplotlib.cm as cm
1718
import matplotlib.colors as mcolors
1819
import matplotlib.collections as mcoll
1920
import matplotlib.font_manager as font_manager
20-
import matplotlib.text as text
2121
import matplotlib.cbook as cbook
2222
import matplotlib.patches as mpatches
2323
import matplotlib.transforms as mtransforms
@@ -31,7 +31,7 @@
3131
# per level.
3232

3333

34-
class ClabelText(text.Text):
34+
class ClabelText(Text):
3535
"""
3636
Unlike the ordinary text, the get_rotation returns an updated
3737
angle in the pixel coordinate assuming that the input rotation is
@@ -253,11 +253,10 @@ def _get_nth_label_width(self, nth):
253253
fig = self.axes.figure
254254
renderer = fig._get_renderer()
255255
return (
256-
text.Text(0, 0,
257-
self.get_text(self.labelLevelList[nth], self.labelFmt),
258-
figure=fig,
259-
size=self.labelFontSizeList[nth],
260-
fontproperties=self.labelFontProps)
256+
Text(0, 0, self.get_text(self.labelLevelList[nth], self.labelFmt),
257+
figure=fig,
258+
size=self.labelFontSizeList[nth],
259+
fontproperties=self.labelFontProps)
261260
.get_window_extent(renderer).width)
262261

263262
@_api.deprecated("3.5")
@@ -267,8 +266,8 @@ def get_label_width(self, lev, fmt, fsize):
267266
lev = self.get_text(lev, fmt)
268267
fig = self.axes.figure
269268
renderer = fig._get_renderer()
270-
width = (text.Text(0, 0, lev, figure=fig,
271-
size=fsize, fontproperties=self.labelFontProps)
269+
width = (Text(0, 0, lev, figure=fig,
270+
size=fsize, fontproperties=self.labelFontProps)
272271
.get_window_extent(renderer).width)
273272
width *= 72 / fig.dpi
274273
return width
@@ -419,10 +418,9 @@ def calc_label_rot_and_inline(self, slc, ind, lw, lc=None, spacing=5):
419418

420419
def _get_label_text(self, x, y, rotation):
421420
dx, dy = self.axes.transData.inverted().transform((x, y))
422-
t = text.Text(dx, dy, rotation=rotation,
423-
horizontalalignment='center',
424-
verticalalignment='center', zorder=self._clabel_zorder)
425-
return t
421+
return Text(dx, dy, rotation=rotation,
422+
horizontalalignment='center',
423+
verticalalignment='center', zorder=self._clabel_zorder)
426424

427425
def _get_label_clabeltext(self, x, y, rotation):
428426
# x, y, rotation is given in pixel coordinate. Convert them to
@@ -585,6 +583,10 @@ def labels(self, inline, inline_spacing):
585583
if inline:
586584
paths[:] = additions
587585

586+
def remove(self):
587+
for text in self.labelTexts:
588+
text.remove()
589+
588590

589591
def _is_closed_polygon(X):
590592
"""
@@ -1389,6 +1391,11 @@ def find_nearest_contour(self, x, y, indices=None, pixel=True):
13891391

13901392
return (conmin, segmin, imin, xmin, ymin, d2min)
13911393

1394+
def remove(self):
1395+
super().remove()
1396+
for coll in self.collections:
1397+
coll.remove()
1398+
13921399

13931400
@_docstring.dedent_interpd
13941401
class QuadContourSet(ContourSet):

lib/matplotlib/patches.py

+4
Original file line numberDiff line numberDiff line change
@@ -3119,6 +3119,10 @@ class ArrowStyle(_Style):
31193119
stroked. This is meant to be used to correct the location of the
31203120
head so that it does not overshoot the destination point, but not all
31213121
classes support it.
3122+
3123+
Examples
3124+
--------
3125+
.. plot:: gallery/text_labels_and_annotations/angles_on_bracket_arrows.py
31223126
"""
31233127

31243128
_style_list = {}

lib/matplotlib/tests/test_contour.py

+10
Original file line numberDiff line numberDiff line change
@@ -682,3 +682,13 @@ def test_negative_linestyles(style):
682682
ax4.clabel(CS4, fontsize=9, inline=True)
683683
ax4.set_title(f'Single color - negative contours {style}')
684684
assert CS4.negative_linestyles == style
685+
686+
687+
def test_contour_remove():
688+
ax = plt.figure().add_subplot()
689+
orig_children = ax.get_children()
690+
cs = ax.contour(np.arange(16).reshape((4, 4)))
691+
cs.clabel()
692+
assert ax.get_children() != orig_children
693+
cs.remove()
694+
assert ax.get_children() == orig_children

0 commit comments

Comments
 (0)