@@ -371,22 +371,215 @@ Angles specified on the *Bracket* arrow styles (``]-[``, ``]-``, ``-[``, or
371
371
applied. Previously, the *angleA * and *angleB * options were allowed, but did
372
372
nothing.
373
373
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
+
374
378
..plot ::
375
379
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
377
384
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)
389
553
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)
390
583
391
584
``TickedStroke `` patheffect
392
585
---------------------------