Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit e1526d1

Browse files
daniel-s-ingramImportanceOfBeingErnestQuLogic
authored
Example showing scale-invariant angle arc (#12518)
* Example showing scale-invariant angle arc * Remove arc based on gid * Made radius scale invariant * Add option for relative units for arc radius * Add option for text * Add annotation to AngleMarker * Separate test from AngleMarker class * finishing angle arc example * Clean up prose in invariant angle marker example. Also, simplify locating some annotations. * Enable annotation clipping on invariant angle example. Otherwise, panning around leaves the angle text visible even outside the Axes. * Fix types. * Rename angle marker example to angle annotation. * Split angle annotation example figure in two. * Fix a few more marker -> annotation. Co-authored-by: ImportanceOfBeingErnest <[email protected]> Co-authored-by: Elliott Sales de Andrade <[email protected]>
1 parent daa5300 commit e1526d1

File tree

1 file changed

+331
-0
lines changed

1 file changed

+331
-0
lines changed
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
"""
2+
===========================
3+
Scale invariant angle label
4+
===========================
5+
6+
This example shows how to create a scale invariant angle annotation. It is
7+
often useful to mark angles between lines or inside shapes with a circular arc.
8+
While Matplotlib provides an `~.patches.Arc`, an inherent problem when directly
9+
using it for such purposes is that an arc being circular in data space is not
10+
necessarily circular in display space. Also, the arc's radius is often best
11+
defined in a coordinate system which is independent of the actual data
12+
coordinates - at least if you want to be able to freely zoom into your plot
13+
without the annotation growing to infinity.
14+
15+
This calls for a solution where the arc's center is defined in data space, but
16+
its radius in a physical unit like points or pixels, or as a ratio of the Axes
17+
dimension. The following ``AngleAnnotation`` class provides such solution.
18+
19+
The example below serves two purposes:
20+
21+
* It provides a ready-to-use solution for the problem of easily drawing angles
22+
in graphs.
23+
* It shows how to subclass a Matplotlib artist to enhance its functionality, as
24+
well as giving a hands-on example on how to use Matplotlib's :doc:`transform
25+
system </tutorials/advanced/transforms_tutorial>`.
26+
27+
If mainly interested in the former, you may copy the below class and jump to
28+
the :ref:`angle-annotation-usage` section.
29+
"""
30+
31+
#########################################################################
32+
# AngleAnnotation class
33+
# ~~~~~~~~~~~~~~~~~~~~~
34+
# The essential idea here is to subclass `~.patches.Arc` and set its transform
35+
# to the `~.transforms.IdentityTransform`, making the parameters of the arc
36+
# defined in pixel space.
37+
# We then override the ``Arc``'s attributes ``_center``, ``theta1``,
38+
# ``theta2``, ``width`` and ``height`` and make them properties, coupling to
39+
# internal methods that calculate the respective parameters each time the
40+
# attribute is accessed and thereby ensuring that the arc in pixel space stays
41+
# synchronized with the input points and size.
42+
# For example, each time the arc's drawing method would query its ``_center``
43+
# attribute, instead of receiving the same number all over again, it will
44+
# instead receive the result of the ``get_center_in_pixels`` method we defined
45+
# in the subclass. This method transforms the center in data coordinates to
46+
# pixels via the Axes transform ``ax.transData``. The size and the angles are
47+
# calculated in a similar fashion, such that the arc changes its shape
48+
# automatically when e.g. zooming or panning interactively.
49+
#
50+
# The functionality of this class allows to annotate the arc with a text. This
51+
# text is a `~.text.Annotation` stored in an attribute ``text``. Since the
52+
# arc's position and radius are defined only at draw time, we need to update
53+
# the text's position accordingly. This is done by reimplementing the ``Arc``'s
54+
# ``draw()`` method to let it call an updating method for the text.
55+
#
56+
# The arc and the text will be added to the provided Axes at instantiation: it
57+
# is hence not strictly necessary to keep a reference to it.
58+
59+
60+
import numpy as np
61+
import matplotlib
62+
import matplotlib.pyplot as plt
63+
from matplotlib.patches import Arc
64+
from matplotlib.transforms import IdentityTransform, TransformedBbox, Bbox
65+
66+
67+
class AngleAnnotation(Arc):
68+
"""
69+
Draws an arc between two vectors which appears circular in display space.
70+
"""
71+
def __init__(self, xy, p1, p2, size=75, unit="points", ax=None,
72+
text="", textposition="inside", text_kw=None, **kwargs):
73+
"""
74+
Parameters
75+
----------
76+
xy, p1, p2 : tuple or array of two floats
77+
Center position and two points. Angle annotation is drawn between
78+
the two vectors connecting *p1* and *p2* with *xy*, respectively.
79+
Units are data coordinates.
80+
81+
size : float
82+
Diameter of the angle annotation in units specified by *unit*.
83+
84+
unit : str
85+
One of the following strings to specify the unit of *size*:
86+
87+
* "pixels": pixels
88+
* "points": points, use points instead of pixels to not have a
89+
dependence on the DPI
90+
* "axes width", "axes height": relative units of Axes width, height
91+
* "axes min", "axes max": minimum or maximum of relative Axes
92+
width, height
93+
94+
ax : `matplotlib.axes.Axes`
95+
The Axes to add the angle annotation to.
96+
97+
text : str
98+
The text to mark the angle with.
99+
100+
textposition : {"inside", "outside", "edge"}
101+
Whether to show the text in- or outside the arc. "edge" can be used
102+
for custom positions anchored at the arc's edge.
103+
104+
text_kw : dict
105+
Dictionary of arguments passed to the Annotation.
106+
107+
**kwargs
108+
Further parameters are passed to `matplotlib.patches.Arc`. Use this
109+
to specify, color, linewidth etc. of the arc.
110+
111+
"""
112+
self.ax = ax or plt.gca()
113+
self._xydata = xy # in data coordinates
114+
self.vec1 = p1
115+
self.vec2 = p2
116+
self.size = size
117+
self.unit = unit
118+
self.textposition = textposition
119+
120+
super().__init__(self._xydata, size, size, angle=0.0,
121+
theta1=self.theta1, theta2=self.theta2, **kwargs)
122+
123+
self.set_transform(IdentityTransform())
124+
self.ax.add_patch(self)
125+
126+
self.kw = dict(ha="center", va="center",
127+
xycoords=IdentityTransform(),
128+
xytext=(0, 0), textcoords="offset points",
129+
annotation_clip=True)
130+
self.kw.update(text_kw or {})
131+
self.text = ax.annotate(text, xy=self._center, **self.kw)
132+
133+
def get_size(self):
134+
factor = 1.
135+
if self.unit == "points":
136+
factor = self.ax.figure.dpi / 72.
137+
elif self.unit[:4] == "axes":
138+
b = TransformedBbox(Bbox.from_bounds(0, 0, 1, 1),
139+
self.ax.transAxes)
140+
dic = {"max": max(b.width, b.height),
141+
"min": min(b.width, b.height),
142+
"width": b.width, "height": b.height}
143+
factor = dic[self.unit[5:]]
144+
return self.size * factor
145+
146+
def set_size(self, size):
147+
self.size = size
148+
149+
def get_center_in_pixels(self):
150+
"""return center in pixels"""
151+
return self.ax.transData.transform(self._xydata)
152+
153+
def set_center(self, xy):
154+
"""set center in data coordinates"""
155+
self._xydata = xy
156+
157+
def get_theta(self, vec):
158+
vec_in_pixels = self.ax.transData.transform(vec) - self._center
159+
return np.rad2deg(np.arctan2(vec_in_pixels[1], vec_in_pixels[0]))
160+
161+
def get_theta1(self):
162+
return self.get_theta(self.vec1)
163+
164+
def get_theta2(self):
165+
return self.get_theta(self.vec2)
166+
167+
def set_theta(self, angle):
168+
pass
169+
170+
# Redefine attributes of the Arc to always give values in pixel space
171+
_center = property(get_center_in_pixels, set_center)
172+
theta1 = property(get_theta1, set_theta)
173+
theta2 = property(get_theta2, set_theta)
174+
width = property(get_size, set_size)
175+
height = property(get_size, set_size)
176+
177+
# The following two methods are needed to update the text position.
178+
def draw(self, renderer):
179+
self.update_text()
180+
super().draw(renderer)
181+
182+
def update_text(self):
183+
c = self._center
184+
s = self.get_size()
185+
angle_span = (self.theta2 - self.theta1) % 360
186+
angle = np.deg2rad(self.theta1 + angle_span / 2)
187+
r = s / 2
188+
if self.textposition == "inside":
189+
r = s / np.interp(angle_span, [60, 90, 135, 180],
190+
[3.3, 3.5, 3.8, 4])
191+
self.text.xy = c + r * np.array([np.cos(angle), np.sin(angle)])
192+
if self.textposition == "outside":
193+
def R90(a, r, w, h):
194+
if a < np.arctan(h/2/(r+w/2)):
195+
return np.sqrt((r+w/2)**2 + (np.tan(a)*(r+w/2))**2)
196+
else:
197+
c = np.sqrt((w/2)**2+(h/2)**2)
198+
T = np.arcsin(c * np.cos(np.pi/2 - a + np.arcsin(h/2/c))/r)
199+
xy = r * np.array([np.cos(a + T), np.sin(a + T)])
200+
xy += np.array([w/2, h/2])
201+
return np.sqrt(np.sum(xy**2))
202+
203+
def R(a, r, w, h):
204+
aa = (a % (np.pi/4))*((a % (np.pi/2)) <= np.pi/4) + \
205+
(np.pi/4 - (a % (np.pi/4)))*((a % (np.pi/2)) >= np.pi/4)
206+
return R90(aa, r, *[w, h][::int(np.sign(np.cos(2*a)))])
207+
208+
bbox = self.text.get_window_extent()
209+
X = R(angle, r, bbox.width, bbox.height)
210+
trans = self.ax.figure.dpi_scale_trans.inverted()
211+
offs = trans.transform(((X-s/2), 0))[0] * 72
212+
self.text.set_position([offs*np.cos(angle), offs*np.sin(angle)])
213+
214+
215+
#########################################################################
216+
# .. _angle-annotation-usage:
217+
#
218+
# Usage
219+
# ~~~~~
220+
#
221+
# Required arguments to ``AngleAnnotation`` are the center of the arc, *xy*,
222+
# and two points, such that the arc spans between the two vectors connecting
223+
# *p1* and *p2* with *xy*, respectively. Those are given in data coordinates.
224+
# Further arguments are the *size* of the arc and its *unit*. Additionally, a
225+
# *text* can be specified, that will be drawn either in- or outside of the arc,
226+
# according to the value of *textposition*. Usage of those arguments is shown
227+
# below.
228+
229+
fig, ax = plt.subplots()
230+
fig.canvas.draw() # Need to draw the figure to define renderer
231+
ax.set_title("AngleLabel example")
232+
233+
# Plot two crossing lines and label each angle between them with the above
234+
# ``AngleAnnotation`` tool.
235+
center = (4.5, 650)
236+
p1 = [(2.5, 710), (6.0, 605)]
237+
p2 = [(3.0, 275), (5.5, 900)]
238+
line1, = ax.plot(*zip(*p1))
239+
line2, = ax.plot(*zip(*p2))
240+
point, = ax.plot(*center, marker="o")
241+
242+
am1 = AngleAnnotation(center, p1[1], p2[1], ax=ax, size=75, text=r"$\alpha$")
243+
am2 = AngleAnnotation(center, p2[1], p1[0], ax=ax, size=35, text=r"$\beta$")
244+
am3 = AngleAnnotation(center, p1[0], p2[0], ax=ax, size=75, text=r"$\gamma$")
245+
am4 = AngleAnnotation(center, p2[0], p1[1], ax=ax, size=35, text=r"$\theta$")
246+
247+
248+
# Showcase some styling options for the angle arc, as well as the text.
249+
p = [(6.0, 400), (5.3, 410), (5.6, 300)]
250+
ax.plot(*zip(*p))
251+
am5 = AngleAnnotation(p[1], p[0], p[2], ax=ax, size=40, text=r"$\Phi$",
252+
linestyle="--", color="gray", textposition="outside",
253+
text_kw=dict(fontsize=16, color="gray"))
254+
255+
256+
#########################################################################
257+
# ``AngleLabel`` options
258+
# ~~~~~~~~~~~~~~~~~~~~~~
259+
#
260+
# The *textposition* and *unit* keyword arguments may be used to modify the
261+
# location of the text label, as shown below:
262+
263+
264+
# Helper function to draw angle easily.
265+
def plot_angle(ax, pos, angle, length=0.95, acol="C0", **kwargs):
266+
vec2 = np.array([np.cos(np.deg2rad(angle)), np.sin(np.deg2rad(angle))])
267+
xy = np.c_[[length, 0], [0, 0], vec2*length].T + np.array(pos)
268+
ax.plot(*xy.T, color=acol)
269+
return AngleAnnotation(pos, xy[0], xy[2], ax=ax, **kwargs)
270+
271+
272+
fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True)
273+
fig.suptitle("AngleLabel keyword arguments")
274+
fig.canvas.draw() # Need to draw the figure to define renderer
275+
276+
# Showcase different text positions.
277+
ax1.margins(y=0.4)
278+
ax1.set_title("textposition")
279+
kw = dict(size=75, unit="points", text=r"$60°$")
280+
281+
am6 = plot_angle(ax1, (2.0, 0), 60, textposition="inside", **kw)
282+
am7 = plot_angle(ax1, (3.5, 0), 60, textposition="outside", **kw)
283+
am8 = plot_angle(ax1, (5.0, 0), 60, textposition="edge",
284+
text_kw=dict(bbox=dict(boxstyle="round", fc="w")), **kw)
285+
am9 = plot_angle(ax1, (6.5, 0), 60, textposition="edge",
286+
text_kw=dict(xytext=(30, 20), arrowprops=dict(arrowstyle="->",
287+
connectionstyle="arc3,rad=-0.2")), **kw)
288+
289+
for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"inside"', '"outside"', '"edge"',
290+
'"edge", custom arrow']):
291+
ax1.annotate(text, xy=(x, 0), xycoords=ax1.get_xaxis_transform(),
292+
bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8,
293+
annotation_clip=True)
294+
295+
# Showcase different size units. The effect of this can best be observed
296+
# by interactively changing the figure size
297+
ax2.margins(y=0.4)
298+
ax2.set_title("unit")
299+
kw = dict(text=r"$60°$", textposition="outside")
300+
301+
am10 = plot_angle(ax2, (2.0, 0), 60, size=50, unit="pixels", **kw)
302+
am11 = plot_angle(ax2, (3.5, 0), 60, size=50, unit="points", **kw)
303+
am12 = plot_angle(ax2, (5.0, 0), 60, size=0.25, unit="axes min", **kw)
304+
am13 = plot_angle(ax2, (6.5, 0), 60, size=0.25, unit="axes max", **kw)
305+
306+
for x, text in zip([2.0, 3.5, 5.0, 6.5], ['"pixels"', '"points"',
307+
'"axes min"', '"axes max"']):
308+
ax2.annotate(text, xy=(x, 0), xycoords=ax2.get_xaxis_transform(),
309+
bbox=dict(boxstyle="round", fc="w"), ha="left", fontsize=8,
310+
annotation_clip=True)
311+
312+
plt.show()
313+
314+
315+
#############################################################################
316+
#
317+
# ------------
318+
#
319+
# References
320+
# """"""""""
321+
#
322+
# The use of the following functions, methods and classes is shown
323+
# in this example:
324+
325+
matplotlib.patches.Arc
326+
matplotlib.axes.Axes.annotate
327+
matplotlib.pyplot.annotate
328+
matplotlib.text.Annotation
329+
matplotlib.transforms.IdentityTransform
330+
matplotlib.transforms.TransformedBbox
331+
matplotlib.transforms.Bbox

0 commit comments

Comments
 (0)