@@ -371,22 +371,215 @@ Angles specified on the *Bracket* arrow styles (``]-[``, ``]-``, ``-[``, or
371371applied. Previously, the *angleA * and *angleB * options were allowed, but did
372372nothing.
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