Skip to content

Commit be2a1a3

Browse files
committed
Updated Angles on Bracket arrow styles example
1 parent 6cc7cda commit be2a1a3

File tree

1 file changed

+205
-12
lines changed

1 file changed

+205
-12
lines changed

doc/users/prev_whats_new/whats_new_3.4.0.rst

Lines changed: 205 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -371,22 +371,215 @@ Angles specified on the *Bracket* arrow styles (``]-[``, ``]-``, ``-[``, or
371371
applied. Previously, the *angleA* and *angleB* options were allowed, but did
372372
nothing.
373373

374+
Angles are annotated using ``AngleAnnotation`` from the example
375+
:doc:`/gallery/text_labels_and_annotations/angle_annotation`. `.FancyArrowPatch`
376+
arrows are added to show the directions of *angleA* and *angleB*.
377+
374378
.. plot::
375379

376-
import matplotlib.patches as mpatches
380+
import matplotlib.pyplot as plt
381+
import numpy as np
382+
from matplotlib.patches import Arc, FancyArrowPatch
383+
from matplotlib.transforms import Bbox, IdentityTransform, TransformedBbox
377384

378-
fig, ax = plt.subplots()
379-
ax.set(xlim=(0, 1), ylim=(-1, 4))
380-
381-
for i, stylename in enumerate((']-[', '|-|')):
382-
for j, angle in enumerate([-30, 60]):
383-
arrowstyle = f'{stylename},angleA={angle},angleB={-angle}'
384-
patch = mpatches.FancyArrowPatch((0.1, 2*i + j), (0.9, 2*i + j),
385-
arrowstyle=arrowstyle,
386-
mutation_scale=25)
387-
ax.text(0.5, 2*i + j, arrowstyle,
388-
verticalalignment='bottom', horizontalalignment='center')
385+
386+
class AngleAnnotation(Arc):
387+
"""
388+
Draws an arc between two vectors which appears circular in display space.
389+
"""
390+
def __init__(self, xy, p1, p2, size=75, unit="points", ax=None,
391+
text="", textposition="inside", text_kw=None, **kwargs):
392+
"""
393+
Parameters
394+
----------
395+
xy, p1, p2 : tuple or array of two floats
396+
Center position and two points. Angle annotation is drawn between
397+
the two vectors connecting *p1* and *p2* with *xy*, respectively.
398+
Units are data coordinates.
399+
400+
size : float
401+
Diameter of the angle annotation in units specified by *unit*.
402+
403+
unit : str
404+
One of the following strings to specify the unit of *size*:
405+
406+
* "pixels": pixels
407+
* "points": points, use points instead of pixels to not have a
408+
dependence on the DPI
409+
* "axes width", "axes height": relative units of Axes width, height
410+
* "axes min", "axes max": minimum or maximum of relative Axes
411+
width, height
412+
413+
ax : `matplotlib.axes.Axes`
414+
The Axes to add the angle annotation to.
415+
416+
text : str
417+
The text to mark the angle with.
418+
419+
textposition : {"inside", "outside", "edge"}
420+
Whether to show the text in- or outside the arc. "edge" can be used
421+
for custom positions anchored at the arc's edge.
422+
423+
text_kw : dict
424+
Dictionary of arguments passed to the Annotation.
425+
426+
**kwargs
427+
Further parameters are passed to `matplotlib.patches.Arc`. Use this
428+
to specify, color, linewidth etc. of the arc.
429+
430+
"""
431+
self.ax = ax or plt.gca()
432+
self._xydata = xy # in data coordinates
433+
self.vec1 = p1
434+
self.vec2 = p2
435+
self.size = size
436+
self.unit = unit
437+
self.textposition = textposition
438+
439+
super().__init__(self._xydata, size, size, angle=0.0,
440+
theta1=self.theta1, theta2=self.theta2, **kwargs)
441+
442+
self.set_transform(IdentityTransform())
443+
self.ax.add_patch(self)
444+
445+
self.kw = dict(ha="center", va="center",
446+
xycoords=IdentityTransform(),
447+
xytext=(0, 0), textcoords="offset points",
448+
annotation_clip=True)
449+
self.kw.update(text_kw or {})
450+
self.text = ax.annotate(text, xy=self._center, **self.kw)
451+
452+
def get_size(self):
453+
factor = 1.
454+
if self.unit == "points":
455+
factor = self.ax.figure.dpi / 72.
456+
elif self.unit[:4] == "axes":
457+
b = TransformedBbox(Bbox.unit(), self.ax.transAxes)
458+
dic = {"max": max(b.width, b.height),
459+
"min": min(b.width, b.height),
460+
"width": b.width, "height": b.height}
461+
factor = dic[self.unit[5:]]
462+
return self.size * factor
463+
464+
def set_size(self, size):
465+
self.size = size
466+
467+
def get_center_in_pixels(self):
468+
"""return center in pixels"""
469+
return self.ax.transData.transform(self._xydata)
470+
471+
def set_center(self, xy):
472+
"""set center in data coordinates"""
473+
self._xydata = xy
474+
475+
def get_theta(self, vec):
476+
vec_in_pixels = self.ax.transData.transform(vec) - self._center
477+
return np.rad2deg(np.arctan2(vec_in_pixels[1], vec_in_pixels[0]))
478+
479+
def get_theta1(self):
480+
return self.get_theta(self.vec1)
481+
482+
def get_theta2(self):
483+
return self.get_theta(self.vec2)
484+
485+
def set_theta(self, angle):
486+
pass
487+
488+
# Redefine attributes of the Arc to always give values in pixel space
489+
_center = property(get_center_in_pixels, set_center)
490+
theta1 = property(get_theta1, set_theta)
491+
theta2 = property(get_theta2, set_theta)
492+
width = property(get_size, set_size)
493+
height = property(get_size, set_size)
494+
495+
# The following two methods are needed to update the text position.
496+
def draw(self, renderer):
497+
self.update_text()
498+
super().draw(renderer)
499+
500+
def update_text(self):
501+
c = self._center
502+
s = self.get_size()
503+
angle_span = (self.theta2 - self.theta1) % 360
504+
angle = np.deg2rad(self.theta1 + angle_span / 2)
505+
r = s / 2
506+
if self.textposition == "inside":
507+
r = s / np.interp(angle_span, [60, 90, 135, 180],
508+
[3.3, 3.5, 3.8, 4])
509+
self.text.xy = c + r * np.array([np.cos(angle), np.sin(angle)])
510+
if self.textposition == "outside":
511+
def R90(a, r, w, h):
512+
if a < np.arctan(h/2/(r+w/2)):
513+
return np.sqrt((r+w/2)**2 + (np.tan(a)*(r+w/2))**2)
514+
else:
515+
c = np.sqrt((w/2)**2+(h/2)**2)
516+
T = np.arcsin(c * np.cos(np.pi/2 - a + np.arcsin(h/2/c))/r)
517+
xy = r * np.array([np.cos(a + T), np.sin(a + T)])
518+
xy += np.array([w/2, h/2])
519+
return np.sqrt(np.sum(xy**2))
520+
521+
def R(a, r, w, h):
522+
aa = (a % (np.pi/4))*((a % (np.pi/2)) <= np.pi/4) + \
523+
(np.pi/4 - (a % (np.pi/4)))*((a % (np.pi/2)) >= np.pi/4)
524+
return R90(aa, r, *[w, h][::int(np.sign(np.cos(2*a)))])
525+
526+
bbox = self.text.get_window_extent()
527+
X = R(angle, r, bbox.width, bbox.height)
528+
trans = self.ax.figure.dpi_scale_trans.inverted()
529+
offs = trans.transform(((X-s/2), 0))[0] * 72
530+
self.text.set_position([offs*np.cos(angle), offs*np.sin(angle)])
531+
532+
533+
def get_point_of_rotated_vertical(origin, line_length, degrees):
534+
"""
535+
Return xy coordinates of the end of a vertical line rotated by degrees.
536+
"""
537+
rad = np.deg2rad(-degrees)
538+
return [origin[0] + line_length * np.sin(rad),
539+
origin[1] + line_length * np.cos(rad)]
540+
541+
542+
fig, ax = plt.subplots(figsize=(8, 7))
543+
ax.set(xlim=(0, 6), ylim=(-1, 4))
544+
545+
for i, stylename in enumerate(["]-[", "|-|"]):
546+
for j, angle in enumerate([-40, 60]):
547+
y = 2*i + j
548+
arrow_centers = [(1, y), (5, y)]
549+
vlines = [[c[0], y + 0.5] for c in arrow_centers]
550+
arrowstyle = f"{stylename},widthA=1.5,widthB=1.5,angleA={angle},angleB={-angle}"
551+
patch = FancyArrowPatch(arrow_centers[0], arrow_centers[1],
552+
arrowstyle=arrowstyle, mutation_scale=25)
389553
ax.add_patch(patch)
554+
ax.text(3, y + 0.05, arrowstyle.replace('widthA=1.5,widthB=1.5,', ''),
555+
verticalalignment="bottom", horizontalalignment="center")
556+
ax.vlines([i[0] for i in vlines], [y, y], [i[1] for i in vlines],
557+
linestyles="--", color="C0")
558+
# Get the coordinates for the drawn patches at A and B
559+
patch_top_coords = [get_point_of_rotated_vertical(arrow_centers[0], 0.5, angle),
560+
get_point_of_rotated_vertical(arrow_centers[1], 0.5, -angle)]
561+
# Create points for annotating A and B with AngleAnnotation
562+
# Points include the top of the vline and patch_top_coords
563+
pointsA =[(1, y + 0.5), patch_top_coords[0]]
564+
pointsB = [patch_top_coords[1], (5, y + 0.5)]
565+
# Define the directions for the arrows for the direction of AngleAnnotation
566+
arrow_angles = [0.5, -0.5]
567+
# Reverse the points and arrow_angles when the angle is negative
568+
if angle < 0:
569+
pointsA.reverse()
570+
pointsB.reverse()
571+
arrow_angles.reverse()
572+
# Add AngleAnnotation and arrows to show angle directions
573+
data = zip(arrow_centers, [pointsA, pointsB], vlines, arrow_angles,
574+
patch_top_coords)
575+
for center, points, vline, arrow_angle, patch_top in data:
576+
am = AngleAnnotation(center, points[0], points[1], ax=ax, size=0.25,
577+
unit="axes min", text=str(-angle))
578+
arrowstyle = "Simple, tail_width=0.5, head_width=4, head_length=8"
579+
kw = dict(arrowstyle=arrowstyle, color="C0")
580+
arrow = FancyArrowPatch(vline, patch_top,
581+
connectionstyle=f"arc3,rad={arrow_angle}", **kw)
582+
ax.add_patch(arrow)
390583
391584
``TickedStroke`` patheffect
392585
---------------------------

0 commit comments

Comments
 (0)