1r"""
2Patches are `.Artist`\s with a face color and an edge color.
3"""
4
5import functools
6import inspect
7import math
8from numbers import Number, Real
9import textwrap
10from types import SimpleNamespace
11from collections import namedtuple
12from matplotlib.transforms import Affine2D
13
14import numpy as np
15
16import matplotlib as mpl
17from . import (_api, artist, cbook, colors, _docstring, hatch as mhatch,
18 lines as mlines, transforms)
19from .bezier import (
20 NonIntersectingPathException, get_cos_sin, get_intersection,
21 get_parallels, inside_circle, make_wedged_bezier2,
22 split_bezier_intersecting_with_closedpath, split_path_inout)
23from .path import Path
24from ._enums import JoinStyle, CapStyle
25
26
27@_docstring.interpd
28@_api.define_aliases({
29 "antialiased": ["aa"],
30 "edgecolor": ["ec"],
31 "facecolor": ["fc"],
32 "linestyle": ["ls"],
33 "linewidth": ["lw"],
34})
35class Patch(artist.Artist):
36 """
37 A patch is a 2D artist with a face color and an edge color.
38
39 If any of *edgecolor*, *facecolor*, *linewidth*, or *antialiased*
40 are *None*, they default to their rc params setting.
41 """
42 zorder = 1
43
44 # Whether to draw an edge by default. Set on a
45 # subclass-by-subclass basis.
46 _edge_default = False
47
48 def __init__(self, *,
49 edgecolor=None,
50 facecolor=None,
51 color=None,
52 linewidth=None,
53 linestyle=None,
54 antialiased=None,
55 hatch=None,
56 fill=True,
57 capstyle=None,
58 joinstyle=None,
59 **kwargs):
60 """
61 The following kwarg properties are supported
62
63 %(Patch:kwdoc)s
64 """
65 super().__init__()
66
67 if linestyle is None:
68 linestyle = "solid"
69 if capstyle is None:
70 capstyle = CapStyle.butt
71 if joinstyle is None:
72 joinstyle = JoinStyle.miter
73
74 self._hatch_color = colors.to_rgba(mpl.rcParams['hatch.color'])
75 self._fill = bool(fill) # needed for set_facecolor call
76 if color is not None:
77 if edgecolor is not None or facecolor is not None:
78 _api.warn_external(
79 "Setting the 'color' property will override "
80 "the edgecolor or facecolor properties.")
81 self.set_color(color)
82 else:
83 self.set_edgecolor(edgecolor)
84 self.set_facecolor(facecolor)
85
86 self._linewidth = 0
87 self._unscaled_dash_pattern = (0, None) # offset, dash
88 self._dash_pattern = (0, None) # offset, dash (scaled by linewidth)
89
90 self.set_linestyle(linestyle)
91 self.set_linewidth(linewidth)
92 self.set_antialiased(antialiased)
93 self.set_hatch(hatch)
94 self.set_capstyle(capstyle)
95 self.set_joinstyle(joinstyle)
96
97 if len(kwargs):
98 self._internal_update(kwargs)
99
100 def get_verts(self):
101 """
102 Return a copy of the vertices used in this patch.
103
104 If the patch contains Bézier curves, the curves will be interpolated by
105 line segments. To access the curves as curves, use `get_path`.
106 """
107 trans = self.get_transform()
108 path = self.get_path()
109 polygons = path.to_polygons(trans)
110 if len(polygons):
111 return polygons[0]
112 return []
113
114 def _process_radius(self, radius):
115 if radius is not None:
116 return radius
117 if isinstance(self._picker, Number):
118 _radius = self._picker
119 else:
120 if self.get_edgecolor()[3] == 0:
121 _radius = 0
122 else:
123 _radius = self.get_linewidth()
124 return _radius
125
126 def contains(self, mouseevent, radius=None):
127 """
128 Test whether the mouse event occurred in the patch.
129
130 Parameters
131 ----------
132 mouseevent : `~matplotlib.backend_bases.MouseEvent`
133 Where the user clicked.
134
135 radius : float, optional
136 Additional margin on the patch in target coordinates of
137 `.Patch.get_transform`. See `.Path.contains_point` for further
138 details.
139
140 If `None`, the default value depends on the state of the object:
141
142 - If `.Artist.get_picker` is a number, the default
143 is that value. This is so that picking works as expected.
144 - Otherwise if the edge color has a non-zero alpha, the default
145 is half of the linewidth. This is so that all the colored
146 pixels are "in" the patch.
147 - Finally, if the edge has 0 alpha, the default is 0. This is
148 so that patches without a stroked edge do not have points
149 outside of the filled region report as "in" due to an
150 invisible edge.
151
152
153 Returns
154 -------
155 (bool, empty dict)
156 """
157 if self._different_canvas(mouseevent):
158 return False, {}
159 radius = self._process_radius(radius)
160 codes = self.get_path().codes
161 if codes is not None:
162 vertices = self.get_path().vertices
163 # if the current path is concatenated by multiple sub paths.
164 # get the indexes of the starting code(MOVETO) of all sub paths
165 idxs, = np.where(codes == Path.MOVETO)
166 # Don't split before the first MOVETO.
167 idxs = idxs[1:]
168 subpaths = map(
169 Path, np.split(vertices, idxs), np.split(codes, idxs))
170 else:
171 subpaths = [self.get_path()]
172 inside = any(
173 subpath.contains_point(
174 (mouseevent.x, mouseevent.y), self.get_transform(), radius)
175 for subpath in subpaths)
176 return inside, {}
177
178 def contains_point(self, point, radius=None):
179 """
180 Return whether the given point is inside the patch.
181
182 Parameters
183 ----------
184 point : (float, float)
185 The point (x, y) to check, in target coordinates of
186 ``.Patch.get_transform()``. These are display coordinates for patches
187 that are added to a figure or Axes.
188 radius : float, optional
189 Additional margin on the patch in target coordinates of
190 `.Patch.get_transform`. See `.Path.contains_point` for further
191 details.
192
193 If `None`, the default value depends on the state of the object:
194
195 - If `.Artist.get_picker` is a number, the default
196 is that value. This is so that picking works as expected.
197 - Otherwise if the edge color has a non-zero alpha, the default
198 is half of the linewidth. This is so that all the colored
199 pixels are "in" the patch.
200 - Finally, if the edge has 0 alpha, the default is 0. This is
201 so that patches without a stroked edge do not have points
202 outside of the filled region report as "in" due to an
203 invisible edge.
204
205 Returns
206 -------
207 bool
208
209 Notes
210 -----
211 The proper use of this method depends on the transform of the patch.
212 Isolated patches do not have a transform. In this case, the patch
213 creation coordinates and the point coordinates match. The following
214 example checks that the center of a circle is within the circle
215
216 >>> center = 0, 0
217 >>> c = Circle(center, radius=1)
218 >>> c.contains_point(center)
219 True
220
221 The convention of checking against the transformed patch stems from
222 the fact that this method is predominantly used to check if display
223 coordinates (e.g. from mouse events) are within the patch. If you want
224 to do the above check with data coordinates, you have to properly
225 transform them first:
226
227 >>> center = 0, 0
228 >>> c = Circle(center, radius=3)
229 >>> plt.gca().add_patch(c)
230 >>> transformed_interior_point = c.get_data_transform().transform((0, 2))
231 >>> c.contains_point(transformed_interior_point)
232 True
233
234 """
235 radius = self._process_radius(radius)
236 return self.get_path().contains_point(point,
237 self.get_transform(),
238 radius)
239
240 def contains_points(self, points, radius=None):
241 """
242 Return whether the given points are inside the patch.
243
244 Parameters
245 ----------
246 points : (N, 2) array
247 The points to check, in target coordinates of
248 ``self.get_transform()``. These are display coordinates for patches
249 that are added to a figure or Axes. Columns contain x and y values.
250 radius : float, optional
251 Additional margin on the patch in target coordinates of
252 `.Patch.get_transform`. See `.Path.contains_point` for further
253 details.
254
255 If `None`, the default value depends on the state of the object:
256
257 - If `.Artist.get_picker` is a number, the default
258 is that value. This is so that picking works as expected.
259 - Otherwise if the edge color has a non-zero alpha, the default
260 is half of the linewidth. This is so that all the colored
261 pixels are "in" the patch.
262 - Finally, if the edge has 0 alpha, the default is 0. This is
263 so that patches without a stroked edge do not have points
264 outside of the filled region report as "in" due to an
265 invisible edge.
266
267 Returns
268 -------
269 length-N bool array
270
271 Notes
272 -----
273 The proper use of this method depends on the transform of the patch.
274 See the notes on `.Patch.contains_point`.
275 """
276 radius = self._process_radius(radius)
277 return self.get_path().contains_points(points,
278 self.get_transform(),
279 radius)
280
281 def update_from(self, other):
282 # docstring inherited.
283 super().update_from(other)
284 # For some properties we don't need or don't want to go through the
285 # getters/setters, so we just copy them directly.
286 self._edgecolor = other._edgecolor
287 self._facecolor = other._facecolor
288 self._original_edgecolor = other._original_edgecolor
289 self._original_facecolor = other._original_facecolor
290 self._fill = other._fill
291 self._hatch = other._hatch
292 self._hatch_color = other._hatch_color
293 self._unscaled_dash_pattern = other._unscaled_dash_pattern
294 self.set_linewidth(other._linewidth) # also sets scaled dashes
295 self.set_transform(other.get_data_transform())
296 # If the transform of other needs further initialization, then it will
297 # be the case for this artist too.
298 self._transformSet = other.is_transform_set()
299
300 def get_extents(self):
301 """
302 Return the `Patch`'s axis-aligned extents as a `~.transforms.Bbox`.
303 """
304 return self.get_path().get_extents(self.get_transform())
305
306 def get_transform(self):
307 """Return the `~.transforms.Transform` applied to the `Patch`."""
308 return self.get_patch_transform() + artist.Artist.get_transform(self)
309
310 def get_data_transform(self):
311 """
312 Return the `~.transforms.Transform` mapping data coordinates to
313 physical coordinates.
314 """
315 return artist.Artist.get_transform(self)
316
317 def get_patch_transform(self):
318 """
319 Return the `~.transforms.Transform` instance mapping patch coordinates
320 to data coordinates.
321
322 For example, one may define a patch of a circle which represents a
323 radius of 5 by providing coordinates for a unit circle, and a
324 transform which scales the coordinates (the patch coordinate) by 5.
325 """
326 return transforms.IdentityTransform()
327
328 def get_antialiased(self):
329 """Return whether antialiasing is used for drawing."""
330 return self._antialiased
331
332 def get_edgecolor(self):
333 """Return the edge color."""
334 return self._edgecolor
335
336 def get_facecolor(self):
337 """Return the face color."""
338 return self._facecolor
339
340 def get_linewidth(self):
341 """Return the line width in points."""
342 return self._linewidth
343
344 def get_linestyle(self):
345 """Return the linestyle."""
346 return self._linestyle
347
348 def set_antialiased(self, aa):
349 """
350 Set whether to use antialiased rendering.
351
352 Parameters
353 ----------
354 aa : bool or None
355 """
356 if aa is None:
357 aa = mpl.rcParams['patch.antialiased']
358 self._antialiased = aa
359 self.stale = True
360
361 def _set_edgecolor(self, color):
362 set_hatch_color = True
363 if color is None:
364 if (mpl.rcParams['patch.force_edgecolor'] or
365 not self._fill or self._edge_default):
366 color = mpl.rcParams['patch.edgecolor']
367 else:
368 color = 'none'
369 set_hatch_color = False
370
371 self._edgecolor = colors.to_rgba(color, self._alpha)
372 if set_hatch_color:
373 self._hatch_color = self._edgecolor
374 self.stale = True
375
376 def set_edgecolor(self, color):
377 """
378 Set the patch edge color.
379
380 Parameters
381 ----------
382 color : :mpltype:`color` or None
383 """
384 self._original_edgecolor = color
385 self._set_edgecolor(color)
386
387 def _set_facecolor(self, color):
388 if color is None:
389 color = mpl.rcParams['patch.facecolor']
390 alpha = self._alpha if self._fill else 0
391 self._facecolor = colors.to_rgba(color, alpha)
392 self.stale = True
393
394 def set_facecolor(self, color):
395 """
396 Set the patch face color.
397
398 Parameters
399 ----------
400 color : :mpltype:`color` or None
401 """
402 self._original_facecolor = color
403 self._set_facecolor(color)
404
405 def set_color(self, c):
406 """
407 Set both the edgecolor and the facecolor.
408
409 Parameters
410 ----------
411 c : :mpltype:`color`
412
413 See Also
414 --------
415 Patch.set_facecolor, Patch.set_edgecolor
416 For setting the edge or face color individually.
417 """
418 self.set_facecolor(c)
419 self.set_edgecolor(c)
420
421 def set_alpha(self, alpha):
422 # docstring inherited
423 super().set_alpha(alpha)
424 self._set_facecolor(self._original_facecolor)
425 self._set_edgecolor(self._original_edgecolor)
426 # stale is already True
427
428 def set_linewidth(self, w):
429 """
430 Set the patch linewidth in points.
431
432 Parameters
433 ----------
434 w : float or None
435 """
436 if w is None:
437 w = mpl.rcParams['patch.linewidth']
438 self._linewidth = float(w)
439 self._dash_pattern = mlines._scale_dashes(
440 *self._unscaled_dash_pattern, w)
441 self.stale = True
442
443 def set_linestyle(self, ls):
444 """
445 Set the patch linestyle.
446
447 ========================================== =================
448 linestyle description
449 ========================================== =================
450 ``'-'`` or ``'solid'`` solid line
451 ``'--'`` or ``'dashed'`` dashed line
452 ``'-.'`` or ``'dashdot'`` dash-dotted line
453 ``':'`` or ``'dotted'`` dotted line
454 ``'none'``, ``'None'``, ``' '``, or ``''`` draw nothing
455 ========================================== =================
456
457 Alternatively a dash tuple of the following form can be provided::
458
459 (offset, onoffseq)
460
461 where ``onoffseq`` is an even length tuple of on and off ink in points.
462
463 Parameters
464 ----------
465 ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...}
466 The line style.
467 """
468 if ls is None:
469 ls = "solid"
470 if ls in [' ', '', 'none']:
471 ls = 'None'
472 self._linestyle = ls
473 self._unscaled_dash_pattern = mlines._get_dash_pattern(ls)
474 self._dash_pattern = mlines._scale_dashes(
475 *self._unscaled_dash_pattern, self._linewidth)
476 self.stale = True
477
478 def set_fill(self, b):
479 """
480 Set whether to fill the patch.
481
482 Parameters
483 ----------
484 b : bool
485 """
486 self._fill = bool(b)
487 self._set_facecolor(self._original_facecolor)
488 self._set_edgecolor(self._original_edgecolor)
489 self.stale = True
490
491 def get_fill(self):
492 """Return whether the patch is filled."""
493 return self._fill
494
495 # Make fill a property so as to preserve the long-standing
496 # but somewhat inconsistent behavior in which fill was an
497 # attribute.
498 fill = property(get_fill, set_fill)
499
500 @_docstring.interpd
501 def set_capstyle(self, s):
502 """
503 Set the `.CapStyle`.
504
505 The default capstyle is 'round' for `.FancyArrowPatch` and 'butt' for
506 all other patches.
507
508 Parameters
509 ----------
510 s : `.CapStyle` or %(CapStyle)s
511 """
512 cs = CapStyle(s)
513 self._capstyle = cs
514 self.stale = True
515
516 def get_capstyle(self):
517 """Return the capstyle."""
518 return self._capstyle.name
519
520 @_docstring.interpd
521 def set_joinstyle(self, s):
522 """
523 Set the `.JoinStyle`.
524
525 The default joinstyle is 'round' for `.FancyArrowPatch` and 'miter' for
526 all other patches.
527
528 Parameters
529 ----------
530 s : `.JoinStyle` or %(JoinStyle)s
531 """
532 js = JoinStyle(s)
533 self._joinstyle = js
534 self.stale = True
535
536 def get_joinstyle(self):
537 """Return the joinstyle."""
538 return self._joinstyle.name
539
540 def set_hatch(self, hatch):
541 r"""
542 Set the hatching pattern.
543
544 *hatch* can be one of::
545
546 / - diagonal hatching
547 \ - back diagonal
548 | - vertical
549 - - horizontal
550 + - crossed
551 x - crossed diagonal
552 o - small circle
553 O - large circle
554 . - dots
555 * - stars
556
557 Letters can be combined, in which case all the specified
558 hatchings are done. If same letter repeats, it increases the
559 density of hatching of that pattern.
560
561 Parameters
562 ----------
563 hatch : {'/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'}
564 """
565 # Use validate_hatch(list) after deprecation.
566 mhatch._validate_hatch_pattern(hatch)
567 self._hatch = hatch
568 self.stale = True
569
570 def get_hatch(self):
571 """Return the hatching pattern."""
572 return self._hatch
573
574 def _draw_paths_with_artist_properties(
575 self, renderer, draw_path_args_list):
576 """
577 ``draw()`` helper factored out for sharing with `FancyArrowPatch`.
578
579 Configure *renderer* and the associated graphics context *gc*
580 from the artist properties, then repeatedly call
581 ``renderer.draw_path(gc, *draw_path_args)`` for each tuple
582 *draw_path_args* in *draw_path_args_list*.
583 """
584
585 renderer.open_group('patch', self.get_gid())
586 gc = renderer.new_gc()
587
588 gc.set_foreground(self._edgecolor, isRGBA=True)
589
590 lw = self._linewidth
591 if self._edgecolor[3] == 0 or self._linestyle == 'None':
592 lw = 0
593 gc.set_linewidth(lw)
594 gc.set_dashes(*self._dash_pattern)
595 gc.set_capstyle(self._capstyle)
596 gc.set_joinstyle(self._joinstyle)
597
598 gc.set_antialiased(self._antialiased)
599 self._set_gc_clip(gc)
600 gc.set_url(self._url)
601 gc.set_snap(self.get_snap())
602
603 gc.set_alpha(self._alpha)
604
605 if self._hatch:
606 gc.set_hatch(self._hatch)
607 gc.set_hatch_color(self._hatch_color)
608
609 if self.get_sketch_params() is not None:
610 gc.set_sketch_params(*self.get_sketch_params())
611
612 if self.get_path_effects():
613 from matplotlib.patheffects import PathEffectRenderer
614 renderer = PathEffectRenderer(self.get_path_effects(), renderer)
615
616 for draw_path_args in draw_path_args_list:
617 renderer.draw_path(gc, *draw_path_args)
618
619 gc.restore()
620 renderer.close_group('patch')
621 self.stale = False
622
623 @artist.allow_rasterization
624 def draw(self, renderer):
625 # docstring inherited
626 if not self.get_visible():
627 return
628 path = self.get_path()
629 transform = self.get_transform()
630 tpath = transform.transform_path_non_affine(path)
631 affine = transform.get_affine()
632 self._draw_paths_with_artist_properties(
633 renderer,
634 [(tpath, affine,
635 # Work around a bug in the PDF and SVG renderers, which
636 # do not draw the hatches if the facecolor is fully
637 # transparent, but do if it is None.
638 self._facecolor if self._facecolor[3] else None)])
639
640 def get_path(self):
641 """Return the path of this patch."""
642 raise NotImplementedError('Derived must override')
643
644 def get_window_extent(self, renderer=None):
645 return self.get_path().get_extents(self.get_transform())
646
647 def _convert_xy_units(self, xy):
648 """Convert x and y units for a tuple (x, y)."""
649 x = self.convert_xunits(xy[0])
650 y = self.convert_yunits(xy[1])
651 return x, y
652
653
654class Shadow(Patch):
655 def __str__(self):
656 return f"Shadow({self.patch})"
657
658 @_docstring.dedent_interpd
659 def __init__(self, patch, ox, oy, *, shade=0.7, **kwargs):
660 """
661 Create a shadow of the given *patch*.
662
663 By default, the shadow will have the same face color as the *patch*,
664 but darkened. The darkness can be controlled by *shade*.
665
666 Parameters
667 ----------
668 patch : `~matplotlib.patches.Patch`
669 The patch to create the shadow for.
670 ox, oy : float
671 The shift of the shadow in data coordinates, scaled by a factor
672 of dpi/72.
673 shade : float, default: 0.7
674 How the darkness of the shadow relates to the original color. If 1, the
675 shadow is black, if 0, the shadow has the same color as the *patch*.
676
677 .. versionadded:: 3.8
678
679 **kwargs
680 Properties of the shadow patch. Supported keys are:
681
682 %(Patch:kwdoc)s
683 """
684 super().__init__()
685 self.patch = patch
686 self._ox, self._oy = ox, oy
687 self._shadow_transform = transforms.Affine2D()
688
689 self.update_from(self.patch)
690 if not 0 <= shade <= 1:
691 raise ValueError("shade must be between 0 and 1.")
692 color = (1 - shade) * np.asarray(colors.to_rgb(self.patch.get_facecolor()))
693 self.update({'facecolor': color, 'edgecolor': color, 'alpha': 0.5,
694 # Place shadow patch directly behind the inherited patch.
695 'zorder': np.nextafter(self.patch.zorder, -np.inf),
696 **kwargs})
697
698 def _update_transform(self, renderer):
699 ox = renderer.points_to_pixels(self._ox)
700 oy = renderer.points_to_pixels(self._oy)
701 self._shadow_transform.clear().translate(ox, oy)
702
703 def get_path(self):
704 return self.patch.get_path()
705
706 def get_patch_transform(self):
707 return self.patch.get_patch_transform() + self._shadow_transform
708
709 def draw(self, renderer):
710 self._update_transform(renderer)
711 super().draw(renderer)
712
713
714class Rectangle(Patch):
715 """
716 A rectangle defined via an anchor point *xy* and its *width* and *height*.
717
718 The rectangle extends from ``xy[0]`` to ``xy[0] + width`` in x-direction
719 and from ``xy[1]`` to ``xy[1] + height`` in y-direction. ::
720
721 : +------------------+
722 : | |
723 : height |
724 : | |
725 : (xy)---- width -----+
726
727 One may picture *xy* as the bottom left corner, but which corner *xy* is
728 actually depends on the direction of the axis and the sign of *width*
729 and *height*; e.g. *xy* would be the bottom right corner if the x-axis
730 was inverted or if *width* was negative.
731 """
732
733 def __str__(self):
734 pars = self._x0, self._y0, self._width, self._height, self.angle
735 fmt = "Rectangle(xy=(%g, %g), width=%g, height=%g, angle=%g)"
736 return fmt % pars
737
738 @_docstring.dedent_interpd
739 def __init__(self, xy, width, height, *,
740 angle=0.0, rotation_point='xy', **kwargs):
741 """
742 Parameters
743 ----------
744 xy : (float, float)
745 The anchor point.
746 width : float
747 Rectangle width.
748 height : float
749 Rectangle height.
750 angle : float, default: 0
751 Rotation in degrees anti-clockwise about the rotation point.
752 rotation_point : {'xy', 'center', (number, number)}, default: 'xy'
753 If ``'xy'``, rotate around the anchor point. If ``'center'`` rotate
754 around the center. If 2-tuple of number, rotate around this
755 coordinate.
756
757 Other Parameters
758 ----------------
759 **kwargs : `~matplotlib.patches.Patch` properties
760 %(Patch:kwdoc)s
761 """
762 super().__init__(**kwargs)
763 self._x0 = xy[0]
764 self._y0 = xy[1]
765 self._width = width
766 self._height = height
767 self.angle = float(angle)
768 self.rotation_point = rotation_point
769 # Required for RectangleSelector with axes aspect ratio != 1
770 # The patch is defined in data coordinates and when changing the
771 # selector with square modifier and not in data coordinates, we need
772 # to correct for the aspect ratio difference between the data and
773 # display coordinate systems. Its value is typically provide by
774 # Axes._get_aspect_ratio()
775 self._aspect_ratio_correction = 1.0
776 self._convert_units() # Validate the inputs.
777
778 def get_path(self):
779 """Return the vertices of the rectangle."""
780 return Path.unit_rectangle()
781
782 def _convert_units(self):
783 """Convert bounds of the rectangle."""
784 x0 = self.convert_xunits(self._x0)
785 y0 = self.convert_yunits(self._y0)
786 x1 = self.convert_xunits(self._x0 + self._width)
787 y1 = self.convert_yunits(self._y0 + self._height)
788 return x0, y0, x1, y1
789
790 def get_patch_transform(self):
791 # Note: This cannot be called until after this has been added to
792 # an Axes, otherwise unit conversion will fail. This makes it very
793 # important to call the accessor method and not directly access the
794 # transformation member variable.
795 bbox = self.get_bbox()
796 if self.rotation_point == 'center':
797 width, height = bbox.x1 - bbox.x0, bbox.y1 - bbox.y0
798 rotation_point = bbox.x0 + width / 2., bbox.y0 + height / 2.
799 elif self.rotation_point == 'xy':
800 rotation_point = bbox.x0, bbox.y0
801 else:
802 rotation_point = self.rotation_point
803 return transforms.BboxTransformTo(bbox) \
804 + transforms.Affine2D() \
805 .translate(-rotation_point[0], -rotation_point[1]) \
806 .scale(1, self._aspect_ratio_correction) \
807 .rotate_deg(self.angle) \
808 .scale(1, 1 / self._aspect_ratio_correction) \
809 .translate(*rotation_point)
810
811 @property
812 def rotation_point(self):
813 """The rotation point of the patch."""
814 return self._rotation_point
815
816 @rotation_point.setter
817 def rotation_point(self, value):
818 if value in ['center', 'xy'] or (
819 isinstance(value, tuple) and len(value) == 2 and
820 isinstance(value[0], Real) and isinstance(value[1], Real)
821 ):
822 self._rotation_point = value
823 else:
824 raise ValueError("`rotation_point` must be one of "
825 "{'xy', 'center', (number, number)}.")
826
827 def get_x(self):
828 """Return the left coordinate of the rectangle."""
829 return self._x0
830
831 def get_y(self):
832 """Return the bottom coordinate of the rectangle."""
833 return self._y0
834
835 def get_xy(self):
836 """Return the left and bottom coords of the rectangle as a tuple."""
837 return self._x0, self._y0
838
839 def get_corners(self):
840 """
841 Return the corners of the rectangle, moving anti-clockwise from
842 (x0, y0).
843 """
844 return self.get_patch_transform().transform(
845 [(0, 0), (1, 0), (1, 1), (0, 1)])
846
847 def get_center(self):
848 """Return the centre of the rectangle."""
849 return self.get_patch_transform().transform((0.5, 0.5))
850
851 def get_width(self):
852 """Return the width of the rectangle."""
853 return self._width
854
855 def get_height(self):
856 """Return the height of the rectangle."""
857 return self._height
858
859 def get_angle(self):
860 """Get the rotation angle in degrees."""
861 return self.angle
862
863 def set_x(self, x):
864 """Set the left coordinate of the rectangle."""
865 self._x0 = x
866 self.stale = True
867
868 def set_y(self, y):
869 """Set the bottom coordinate of the rectangle."""
870 self._y0 = y
871 self.stale = True
872
873 def set_angle(self, angle):
874 """
875 Set the rotation angle in degrees.
876
877 The rotation is performed anti-clockwise around *xy*.
878 """
879 self.angle = angle
880 self.stale = True
881
882 def set_xy(self, xy):
883 """
884 Set the left and bottom coordinates of the rectangle.
885
886 Parameters
887 ----------
888 xy : (float, float)
889 """
890 self._x0, self._y0 = xy
891 self.stale = True
892
893 def set_width(self, w):
894 """Set the width of the rectangle."""
895 self._width = w
896 self.stale = True
897
898 def set_height(self, h):
899 """Set the height of the rectangle."""
900 self._height = h
901 self.stale = True
902
903 def set_bounds(self, *args):
904 """
905 Set the bounds of the rectangle as *left*, *bottom*, *width*, *height*.
906
907 The values may be passed as separate parameters or as a tuple::
908
909 set_bounds(left, bottom, width, height)
910 set_bounds((left, bottom, width, height))
911
912 .. ACCEPTS: (left, bottom, width, height)
913 """
914 if len(args) == 1:
915 l, b, w, h = args[0]
916 else:
917 l, b, w, h = args
918 self._x0 = l
919 self._y0 = b
920 self._width = w
921 self._height = h
922 self.stale = True
923
924 def get_bbox(self):
925 """Return the `.Bbox`."""
926 return transforms.Bbox.from_extents(*self._convert_units())
927
928 xy = property(get_xy, set_xy)
929
930
931class RegularPolygon(Patch):
932 """A regular polygon patch."""
933
934 def __str__(self):
935 s = "RegularPolygon((%g, %g), %d, radius=%g, orientation=%g)"
936 return s % (self.xy[0], self.xy[1], self.numvertices, self.radius,
937 self.orientation)
938
939 @_docstring.dedent_interpd
940 def __init__(self, xy, numVertices, *,
941 radius=5, orientation=0, **kwargs):
942 """
943 Parameters
944 ----------
945 xy : (float, float)
946 The center position.
947
948 numVertices : int
949 The number of vertices.
950
951 radius : float
952 The distance from the center to each of the vertices.
953
954 orientation : float
955 The polygon rotation angle (in radians).
956
957 **kwargs
958 `Patch` properties:
959
960 %(Patch:kwdoc)s
961 """
962 self.xy = xy
963 self.numvertices = numVertices
964 self.orientation = orientation
965 self.radius = radius
966 self._path = Path.unit_regular_polygon(numVertices)
967 self._patch_transform = transforms.Affine2D()
968 super().__init__(**kwargs)
969
970 def get_path(self):
971 return self._path
972
973 def get_patch_transform(self):
974 return self._patch_transform.clear() \
975 .scale(self.radius) \
976 .rotate(self.orientation) \
977 .translate(*self.xy)
978
979
980class PathPatch(Patch):
981 """A general polycurve path patch."""
982
983 _edge_default = True
984
985 def __str__(self):
986 s = "PathPatch%d((%g, %g) ...)"
987 return s % (len(self._path.vertices), *tuple(self._path.vertices[0]))
988
989 @_docstring.dedent_interpd
990 def __init__(self, path, **kwargs):
991 """
992 *path* is a `.Path` object.
993
994 Valid keyword arguments are:
995
996 %(Patch:kwdoc)s
997 """
998 super().__init__(**kwargs)
999 self._path = path
1000
1001 def get_path(self):
1002 return self._path
1003
1004 def set_path(self, path):
1005 self._path = path
1006
1007
1008class StepPatch(PathPatch):
1009 """
1010 A path patch describing a stepwise constant function.
1011
1012 By default, the path is not closed and starts and stops at
1013 baseline value.
1014 """
1015
1016 _edge_default = False
1017
1018 @_docstring.dedent_interpd
1019 def __init__(self, values, edges, *,
1020 orientation='vertical', baseline=0, **kwargs):
1021 """
1022 Parameters
1023 ----------
1024 values : array-like
1025 The step heights.
1026
1027 edges : array-like
1028 The edge positions, with ``len(edges) == len(vals) + 1``,
1029 between which the curve takes on vals values.
1030
1031 orientation : {'vertical', 'horizontal'}, default: 'vertical'
1032 The direction of the steps. Vertical means that *values* are
1033 along the y-axis, and edges are along the x-axis.
1034
1035 baseline : float, array-like or None, default: 0
1036 The bottom value of the bounding edges or when
1037 ``fill=True``, position of lower edge. If *fill* is
1038 True or an array is passed to *baseline*, a closed
1039 path is drawn.
1040
1041 **kwargs
1042 `Patch` properties:
1043
1044 %(Patch:kwdoc)s
1045 """
1046 self.orientation = orientation
1047 self._edges = np.asarray(edges)
1048 self._values = np.asarray(values)
1049 self._baseline = np.asarray(baseline) if baseline is not None else None
1050 self._update_path()
1051 super().__init__(self._path, **kwargs)
1052
1053 def _update_path(self):
1054 if np.isnan(np.sum(self._edges)):
1055 raise ValueError('Nan values in "edges" are disallowed')
1056 if self._edges.size - 1 != self._values.size:
1057 raise ValueError('Size mismatch between "values" and "edges". '
1058 "Expected `len(values) + 1 == len(edges)`, but "
1059 f"`len(values) = {self._values.size}` and "
1060 f"`len(edges) = {self._edges.size}`.")
1061 # Initializing with empty arrays allows supporting empty stairs.
1062 verts, codes = [np.empty((0, 2))], [np.empty(0, dtype=Path.code_type)]
1063
1064 _nan_mask = np.isnan(self._values)
1065 if self._baseline is not None:
1066 _nan_mask |= np.isnan(self._baseline)
1067 for idx0, idx1 in cbook.contiguous_regions(~_nan_mask):
1068 x = np.repeat(self._edges[idx0:idx1+1], 2)
1069 y = np.repeat(self._values[idx0:idx1], 2)
1070 if self._baseline is None:
1071 y = np.concatenate([y[:1], y, y[-1:]])
1072 elif self._baseline.ndim == 0: # single baseline value
1073 y = np.concatenate([[self._baseline], y, [self._baseline]])
1074 elif self._baseline.ndim == 1: # baseline array
1075 base = np.repeat(self._baseline[idx0:idx1], 2)[::-1]
1076 x = np.concatenate([x, x[::-1]])
1077 y = np.concatenate([base[-1:], y, base[:1],
1078 base[:1], base, base[-1:]])
1079 else: # no baseline
1080 raise ValueError('Invalid `baseline` specified')
1081 if self.orientation == 'vertical':
1082 xy = np.column_stack([x, y])
1083 else:
1084 xy = np.column_stack([y, x])
1085 verts.append(xy)
1086 codes.append([Path.MOVETO] + [Path.LINETO]*(len(xy)-1))
1087 self._path = Path(np.concatenate(verts), np.concatenate(codes))
1088
1089 def get_data(self):
1090 """Get `.StepPatch` values, edges and baseline as namedtuple."""
1091 StairData = namedtuple('StairData', 'values edges baseline')
1092 return StairData(self._values, self._edges, self._baseline)
1093
1094 def set_data(self, values=None, edges=None, baseline=None):
1095 """
1096 Set `.StepPatch` values, edges and baseline.
1097
1098 Parameters
1099 ----------
1100 values : 1D array-like or None
1101 Will not update values, if passing None
1102 edges : 1D array-like, optional
1103 baseline : float, 1D array-like or None
1104 """
1105 if values is None and edges is None and baseline is None:
1106 raise ValueError("Must set *values*, *edges* or *baseline*.")
1107 if values is not None:
1108 self._values = np.asarray(values)
1109 if edges is not None:
1110 self._edges = np.asarray(edges)
1111 if baseline is not None:
1112 self._baseline = np.asarray(baseline)
1113 self._update_path()
1114 self.stale = True
1115
1116
1117class Polygon(Patch):
1118 """A general polygon patch."""
1119
1120 def __str__(self):
1121 if len(self._path.vertices):
1122 s = "Polygon%d((%g, %g) ...)"
1123 return s % (len(self._path.vertices), *self._path.vertices[0])
1124 else:
1125 return "Polygon0()"
1126
1127 @_docstring.dedent_interpd
1128 def __init__(self, xy, *, closed=True, **kwargs):
1129 """
1130 Parameters
1131 ----------
1132 xy : (N, 2) array
1133
1134 closed : bool, default: True
1135 Whether the polygon is closed (i.e., has identical start and end
1136 points).
1137
1138 **kwargs
1139 %(Patch:kwdoc)s
1140 """
1141 super().__init__(**kwargs)
1142 self._closed = closed
1143 self.set_xy(xy)
1144
1145 def get_path(self):
1146 """Get the `.Path` of the polygon."""
1147 return self._path
1148
1149 def get_closed(self):
1150 """Return whether the polygon is closed."""
1151 return self._closed
1152
1153 def set_closed(self, closed):
1154 """
1155 Set whether the polygon is closed.
1156
1157 Parameters
1158 ----------
1159 closed : bool
1160 True if the polygon is closed
1161 """
1162 if self._closed == bool(closed):
1163 return
1164 self._closed = bool(closed)
1165 self.set_xy(self.get_xy())
1166 self.stale = True
1167
1168 def get_xy(self):
1169 """
1170 Get the vertices of the path.
1171
1172 Returns
1173 -------
1174 (N, 2) array
1175 The coordinates of the vertices.
1176 """
1177 return self._path.vertices
1178
1179 def set_xy(self, xy):
1180 """
1181 Set the vertices of the polygon.
1182
1183 Parameters
1184 ----------
1185 xy : (N, 2) array-like
1186 The coordinates of the vertices.
1187
1188 Notes
1189 -----
1190 Unlike `.Path`, we do not ignore the last input vertex. If the
1191 polygon is meant to be closed, and the last point of the polygon is not
1192 equal to the first, we assume that the user has not explicitly passed a
1193 ``CLOSEPOLY`` vertex, and add it ourselves.
1194 """
1195 xy = np.asarray(xy)
1196 nverts, _ = xy.shape
1197 if self._closed:
1198 # if the first and last vertex are the "same", then we assume that
1199 # the user explicitly passed the CLOSEPOLY vertex. Otherwise, we
1200 # have to append one since the last vertex will be "ignored" by
1201 # Path
1202 if nverts == 1 or nverts > 1 and (xy[0] != xy[-1]).any():
1203 xy = np.concatenate([xy, [xy[0]]])
1204 else:
1205 # if we aren't closed, and the last vertex matches the first, then
1206 # we assume we have an unnecessary CLOSEPOLY vertex and remove it
1207 if nverts > 2 and (xy[0] == xy[-1]).all():
1208 xy = xy[:-1]
1209 self._path = Path(xy, closed=self._closed)
1210 self.stale = True
1211
1212 xy = property(get_xy, set_xy,
1213 doc='The vertices of the path as a (N, 2) array.')
1214
1215
1216class Wedge(Patch):
1217 """Wedge shaped patch."""
1218
1219 def __str__(self):
1220 pars = (self.center[0], self.center[1], self.r,
1221 self.theta1, self.theta2, self.width)
1222 fmt = "Wedge(center=(%g, %g), r=%g, theta1=%g, theta2=%g, width=%s)"
1223 return fmt % pars
1224
1225 @_docstring.dedent_interpd
1226 def __init__(self, center, r, theta1, theta2, *, width=None, **kwargs):
1227 """
1228 A wedge centered at *x*, *y* center with radius *r* that
1229 sweeps *theta1* to *theta2* (in degrees). If *width* is given,
1230 then a partial wedge is drawn from inner radius *r* - *width*
1231 to outer radius *r*.
1232
1233 Valid keyword arguments are:
1234
1235 %(Patch:kwdoc)s
1236 """
1237 super().__init__(**kwargs)
1238 self.center = center
1239 self.r, self.width = r, width
1240 self.theta1, self.theta2 = theta1, theta2
1241 self._patch_transform = transforms.IdentityTransform()
1242 self._recompute_path()
1243
1244 def _recompute_path(self):
1245 # Inner and outer rings are connected unless the annulus is complete
1246 if abs((self.theta2 - self.theta1) - 360) <= 1e-12:
1247 theta1, theta2 = 0, 360
1248 connector = Path.MOVETO
1249 else:
1250 theta1, theta2 = self.theta1, self.theta2
1251 connector = Path.LINETO
1252
1253 # Form the outer ring
1254 arc = Path.arc(theta1, theta2)
1255
1256 if self.width is not None:
1257 # Partial annulus needs to draw the outer ring
1258 # followed by a reversed and scaled inner ring
1259 v1 = arc.vertices
1260 v2 = arc.vertices[::-1] * (self.r - self.width) / self.r
1261 v = np.concatenate([v1, v2, [(0, 0)]])
1262 c = [*arc.codes, connector, *arc.codes[1:], Path.CLOSEPOLY]
1263 else:
1264 # Wedge doesn't need an inner ring
1265 v = np.concatenate([arc.vertices, [(0, 0), (0, 0)]])
1266 c = [*arc.codes, connector, Path.CLOSEPOLY]
1267
1268 # Shift and scale the wedge to the final location.
1269 self._path = Path(v * self.r + self.center, c)
1270
1271 def set_center(self, center):
1272 self._path = None
1273 self.center = center
1274 self.stale = True
1275
1276 def set_radius(self, radius):
1277 self._path = None
1278 self.r = radius
1279 self.stale = True
1280
1281 def set_theta1(self, theta1):
1282 self._path = None
1283 self.theta1 = theta1
1284 self.stale = True
1285
1286 def set_theta2(self, theta2):
1287 self._path = None
1288 self.theta2 = theta2
1289 self.stale = True
1290
1291 def set_width(self, width):
1292 self._path = None
1293 self.width = width
1294 self.stale = True
1295
1296 def get_path(self):
1297 if self._path is None:
1298 self._recompute_path()
1299 return self._path
1300
1301
1302# COVERAGE NOTE: Not used internally or from examples
1303class Arrow(Patch):
1304 """An arrow patch."""
1305
1306 def __str__(self):
1307 return "Arrow()"
1308
1309 _path = Path._create_closed([
1310 [0.0, 0.1], [0.0, -0.1], [0.8, -0.1], [0.8, -0.3], [1.0, 0.0],
1311 [0.8, 0.3], [0.8, 0.1]])
1312
1313 @_docstring.dedent_interpd
1314 def __init__(self, x, y, dx, dy, *, width=1.0, **kwargs):
1315 """
1316 Draws an arrow from (*x*, *y*) to (*x* + *dx*, *y* + *dy*).
1317 The width of the arrow is scaled by *width*.
1318
1319 Parameters
1320 ----------
1321 x : float
1322 x coordinate of the arrow tail.
1323 y : float
1324 y coordinate of the arrow tail.
1325 dx : float
1326 Arrow length in the x direction.
1327 dy : float
1328 Arrow length in the y direction.
1329 width : float, default: 1
1330 Scale factor for the width of the arrow. With a default value of 1,
1331 the tail width is 0.2 and head width is 0.6.
1332 **kwargs
1333 Keyword arguments control the `Patch` properties:
1334
1335 %(Patch:kwdoc)s
1336
1337 See Also
1338 --------
1339 FancyArrow
1340 Patch that allows independent control of the head and tail
1341 properties.
1342 """
1343 super().__init__(**kwargs)
1344 self.set_data(x, y, dx, dy, width)
1345
1346 def get_path(self):
1347 return self._path
1348
1349 def get_patch_transform(self):
1350 return self._patch_transform
1351
1352 def set_data(self, x=None, y=None, dx=None, dy=None, width=None):
1353 """
1354 Set `.Arrow` x, y, dx, dy and width.
1355 Values left as None will not be updated.
1356
1357 Parameters
1358 ----------
1359 x, y : float or None, default: None
1360 The x and y coordinates of the arrow base.
1361
1362 dx, dy : float or None, default: None
1363 The length of the arrow along x and y direction.
1364
1365 width : float or None, default: None
1366 Width of full arrow tail.
1367 """
1368 if x is not None:
1369 self._x = x
1370 if y is not None:
1371 self._y = y
1372 if dx is not None:
1373 self._dx = dx
1374 if dy is not None:
1375 self._dy = dy
1376 if width is not None:
1377 self._width = width
1378 self._patch_transform = (
1379 transforms.Affine2D()
1380 .scale(np.hypot(self._dx, self._dy), self._width)
1381 .rotate(np.arctan2(self._dy, self._dx))
1382 .translate(self._x, self._y)
1383 .frozen())
1384
1385
1386class FancyArrow(Polygon):
1387 """
1388 Like Arrow, but lets you set head width and head height independently.
1389 """
1390
1391 _edge_default = True
1392
1393 def __str__(self):
1394 return "FancyArrow()"
1395
1396 @_docstring.dedent_interpd
1397 def __init__(self, x, y, dx, dy, *,
1398 width=0.001, length_includes_head=False, head_width=None,
1399 head_length=None, shape='full', overhang=0,
1400 head_starts_at_zero=False, **kwargs):
1401 """
1402 Parameters
1403 ----------
1404 x, y : float
1405 The x and y coordinates of the arrow base.
1406
1407 dx, dy : float
1408 The length of the arrow along x and y direction.
1409
1410 width : float, default: 0.001
1411 Width of full arrow tail.
1412
1413 length_includes_head : bool, default: False
1414 True if head is to be counted in calculating the length.
1415
1416 head_width : float or None, default: 3*width
1417 Total width of the full arrow head.
1418
1419 head_length : float or None, default: 1.5*head_width
1420 Length of arrow head.
1421
1422 shape : {'full', 'left', 'right'}, default: 'full'
1423 Draw the left-half, right-half, or full arrow.
1424
1425 overhang : float, default: 0
1426 Fraction that the arrow is swept back (0 overhang means
1427 triangular shape). Can be negative or greater than one.
1428
1429 head_starts_at_zero : bool, default: False
1430 If True, the head starts being drawn at coordinate 0
1431 instead of ending at coordinate 0.
1432
1433 **kwargs
1434 `.Patch` properties:
1435
1436 %(Patch:kwdoc)s
1437 """
1438 self._x = x
1439 self._y = y
1440 self._dx = dx
1441 self._dy = dy
1442 self._width = width
1443 self._length_includes_head = length_includes_head
1444 self._head_width = head_width
1445 self._head_length = head_length
1446 self._shape = shape
1447 self._overhang = overhang
1448 self._head_starts_at_zero = head_starts_at_zero
1449 self._make_verts()
1450 super().__init__(self.verts, closed=True, **kwargs)
1451
1452 def set_data(self, *, x=None, y=None, dx=None, dy=None, width=None,
1453 head_width=None, head_length=None):
1454 """
1455 Set `.FancyArrow` x, y, dx, dy, width, head_with, and head_length.
1456 Values left as None will not be updated.
1457
1458 Parameters
1459 ----------
1460 x, y : float or None, default: None
1461 The x and y coordinates of the arrow base.
1462
1463 dx, dy : float or None, default: None
1464 The length of the arrow along x and y direction.
1465
1466 width : float or None, default: None
1467 Width of full arrow tail.
1468
1469 head_width : float or None, default: None
1470 Total width of the full arrow head.
1471
1472 head_length : float or None, default: None
1473 Length of arrow head.
1474 """
1475 if x is not None:
1476 self._x = x
1477 if y is not None:
1478 self._y = y
1479 if dx is not None:
1480 self._dx = dx
1481 if dy is not None:
1482 self._dy = dy
1483 if width is not None:
1484 self._width = width
1485 if head_width is not None:
1486 self._head_width = head_width
1487 if head_length is not None:
1488 self._head_length = head_length
1489 self._make_verts()
1490 self.set_xy(self.verts)
1491
1492 def _make_verts(self):
1493 if self._head_width is None:
1494 head_width = 3 * self._width
1495 else:
1496 head_width = self._head_width
1497 if self._head_length is None:
1498 head_length = 1.5 * head_width
1499 else:
1500 head_length = self._head_length
1501
1502 distance = np.hypot(self._dx, self._dy)
1503
1504 if self._length_includes_head:
1505 length = distance
1506 else:
1507 length = distance + head_length
1508 if not length:
1509 self.verts = np.empty([0, 2]) # display nothing if empty
1510 else:
1511 # start by drawing horizontal arrow, point at (0, 0)
1512 hw, hl = head_width, head_length
1513 hs, lw = self._overhang, self._width
1514 left_half_arrow = np.array([
1515 [0.0, 0.0], # tip
1516 [-hl, -hw / 2], # leftmost
1517 [-hl * (1 - hs), -lw / 2], # meets stem
1518 [-length, -lw / 2], # bottom left
1519 [-length, 0],
1520 ])
1521 # if we're not including the head, shift up by head length
1522 if not self._length_includes_head:
1523 left_half_arrow += [head_length, 0]
1524 # if the head starts at 0, shift up by another head length
1525 if self._head_starts_at_zero:
1526 left_half_arrow += [head_length / 2, 0]
1527 # figure out the shape, and complete accordingly
1528 if self._shape == 'left':
1529 coords = left_half_arrow
1530 else:
1531 right_half_arrow = left_half_arrow * [1, -1]
1532 if self._shape == 'right':
1533 coords = right_half_arrow
1534 elif self._shape == 'full':
1535 # The half-arrows contain the midpoint of the stem,
1536 # which we can omit from the full arrow. Including it
1537 # twice caused a problem with xpdf.
1538 coords = np.concatenate([left_half_arrow[:-1],
1539 right_half_arrow[-2::-1]])
1540 else:
1541 raise ValueError(f"Got unknown shape: {self._shape!r}")
1542 if distance != 0:
1543 cx = self._dx / distance
1544 sx = self._dy / distance
1545 else:
1546 # Account for division by zero
1547 cx, sx = 0, 1
1548 M = [[cx, sx], [-sx, cx]]
1549 self.verts = np.dot(coords, M) + [
1550 self._x + self._dx,
1551 self._y + self._dy,
1552 ]
1553
1554
1555_docstring.interpd.update(
1556 FancyArrow="\n".join(
1557 (inspect.getdoc(FancyArrow.__init__) or "").splitlines()[2:]))
1558
1559
1560class CirclePolygon(RegularPolygon):
1561 """A polygon-approximation of a circle patch."""
1562
1563 def __str__(self):
1564 s = "CirclePolygon((%g, %g), radius=%g, resolution=%d)"
1565 return s % (self.xy[0], self.xy[1], self.radius, self.numvertices)
1566
1567 @_docstring.dedent_interpd
1568 def __init__(self, xy, radius=5, *,
1569 resolution=20, # the number of vertices
1570 ** kwargs):
1571 """
1572 Create a circle at *xy* = (*x*, *y*) with given *radius*.
1573
1574 This circle is approximated by a regular polygon with *resolution*
1575 sides. For a smoother circle drawn with splines, see `Circle`.
1576
1577 Valid keyword arguments are:
1578
1579 %(Patch:kwdoc)s
1580 """
1581 super().__init__(
1582 xy, resolution, radius=radius, orientation=0, **kwargs)
1583
1584
1585class Ellipse(Patch):
1586 """A scale-free ellipse."""
1587
1588 def __str__(self):
1589 pars = (self._center[0], self._center[1],
1590 self.width, self.height, self.angle)
1591 fmt = "Ellipse(xy=(%s, %s), width=%s, height=%s, angle=%s)"
1592 return fmt % pars
1593
1594 @_docstring.dedent_interpd
1595 def __init__(self, xy, width, height, *, angle=0, **kwargs):
1596 """
1597 Parameters
1598 ----------
1599 xy : (float, float)
1600 xy coordinates of ellipse centre.
1601 width : float
1602 Total length (diameter) of horizontal axis.
1603 height : float
1604 Total length (diameter) of vertical axis.
1605 angle : float, default: 0
1606 Rotation in degrees anti-clockwise.
1607
1608 Notes
1609 -----
1610 Valid keyword arguments are:
1611
1612 %(Patch:kwdoc)s
1613 """
1614 super().__init__(**kwargs)
1615
1616 self._center = xy
1617 self._width, self._height = width, height
1618 self._angle = angle
1619 self._path = Path.unit_circle()
1620 # Required for EllipseSelector with axes aspect ratio != 1
1621 # The patch is defined in data coordinates and when changing the
1622 # selector with square modifier and not in data coordinates, we need
1623 # to correct for the aspect ratio difference between the data and
1624 # display coordinate systems.
1625 self._aspect_ratio_correction = 1.0
1626 # Note: This cannot be calculated until this is added to an Axes
1627 self._patch_transform = transforms.IdentityTransform()
1628
1629 def _recompute_transform(self):
1630 """
1631 Notes
1632 -----
1633 This cannot be called until after this has been added to an Axes,
1634 otherwise unit conversion will fail. This makes it very important to
1635 call the accessor method and not directly access the transformation
1636 member variable.
1637 """
1638 center = (self.convert_xunits(self._center[0]),
1639 self.convert_yunits(self._center[1]))
1640 width = self.convert_xunits(self._width)
1641 height = self.convert_yunits(self._height)
1642 self._patch_transform = transforms.Affine2D() \
1643 .scale(width * 0.5, height * 0.5 * self._aspect_ratio_correction) \
1644 .rotate_deg(self.angle) \
1645 .scale(1, 1 / self._aspect_ratio_correction) \
1646 .translate(*center)
1647
1648 def get_path(self):
1649 """Return the path of the ellipse."""
1650 return self._path
1651
1652 def get_patch_transform(self):
1653 self._recompute_transform()
1654 return self._patch_transform
1655
1656 def set_center(self, xy):
1657 """
1658 Set the center of the ellipse.
1659
1660 Parameters
1661 ----------
1662 xy : (float, float)
1663 """
1664 self._center = xy
1665 self.stale = True
1666
1667 def get_center(self):
1668 """Return the center of the ellipse."""
1669 return self._center
1670
1671 center = property(get_center, set_center)
1672
1673 def set_width(self, width):
1674 """
1675 Set the width of the ellipse.
1676
1677 Parameters
1678 ----------
1679 width : float
1680 """
1681 self._width = width
1682 self.stale = True
1683
1684 def get_width(self):
1685 """
1686 Return the width of the ellipse.
1687 """
1688 return self._width
1689
1690 width = property(get_width, set_width)
1691
1692 def set_height(self, height):
1693 """
1694 Set the height of the ellipse.
1695
1696 Parameters
1697 ----------
1698 height : float
1699 """
1700 self._height = height
1701 self.stale = True
1702
1703 def get_height(self):
1704 """Return the height of the ellipse."""
1705 return self._height
1706
1707 height = property(get_height, set_height)
1708
1709 def set_angle(self, angle):
1710 """
1711 Set the angle of the ellipse.
1712
1713 Parameters
1714 ----------
1715 angle : float
1716 """
1717 self._angle = angle
1718 self.stale = True
1719
1720 def get_angle(self):
1721 """Return the angle of the ellipse."""
1722 return self._angle
1723
1724 angle = property(get_angle, set_angle)
1725
1726 def get_corners(self):
1727 """
1728 Return the corners of the ellipse bounding box.
1729
1730 The bounding box orientation is moving anti-clockwise from the
1731 lower left corner defined before rotation.
1732 """
1733 return self.get_patch_transform().transform(
1734 [(-1, -1), (1, -1), (1, 1), (-1, 1)])
1735
1736 def get_vertices(self):
1737 """
1738 Return the vertices coordinates of the ellipse.
1739
1740 The definition can be found `here <https://en.wikipedia.org/wiki/Ellipse>`_
1741
1742 .. versionadded:: 3.8
1743 """
1744 if self.width < self.height:
1745 ret = self.get_patch_transform().transform([(0, 1), (0, -1)])
1746 else:
1747 ret = self.get_patch_transform().transform([(1, 0), (-1, 0)])
1748 return [tuple(x) for x in ret]
1749
1750 def get_co_vertices(self):
1751 """
1752 Return the co-vertices coordinates of the ellipse.
1753
1754 The definition can be found `here <https://en.wikipedia.org/wiki/Ellipse>`_
1755
1756 .. versionadded:: 3.8
1757 """
1758 if self.width < self.height:
1759 ret = self.get_patch_transform().transform([(1, 0), (-1, 0)])
1760 else:
1761 ret = self.get_patch_transform().transform([(0, 1), (0, -1)])
1762 return [tuple(x) for x in ret]
1763
1764
1765class Annulus(Patch):
1766 """
1767 An elliptical annulus.
1768 """
1769
1770 @_docstring.dedent_interpd
1771 def __init__(self, xy, r, width, angle=0.0, **kwargs):
1772 """
1773 Parameters
1774 ----------
1775 xy : (float, float)
1776 xy coordinates of annulus centre.
1777 r : float or (float, float)
1778 The radius, or semi-axes:
1779
1780 - If float: radius of the outer circle.
1781 - If two floats: semi-major and -minor axes of outer ellipse.
1782 width : float
1783 Width (thickness) of the annular ring. The width is measured inward
1784 from the outer ellipse so that for the inner ellipse the semi-axes
1785 are given by ``r - width``. *width* must be less than or equal to
1786 the semi-minor axis.
1787 angle : float, default: 0
1788 Rotation angle in degrees (anti-clockwise from the positive
1789 x-axis). Ignored for circular annuli (i.e., if *r* is a scalar).
1790 **kwargs
1791 Keyword arguments control the `Patch` properties:
1792
1793 %(Patch:kwdoc)s
1794 """
1795 super().__init__(**kwargs)
1796
1797 self.set_radii(r)
1798 self.center = xy
1799 self.width = width
1800 self.angle = angle
1801 self._path = None
1802
1803 def __str__(self):
1804 if self.a == self.b:
1805 r = self.a
1806 else:
1807 r = (self.a, self.b)
1808
1809 return "Annulus(xy=(%s, %s), r=%s, width=%s, angle=%s)" % \
1810 (*self.center, r, self.width, self.angle)
1811
1812 def set_center(self, xy):
1813 """
1814 Set the center of the annulus.
1815
1816 Parameters
1817 ----------
1818 xy : (float, float)
1819 """
1820 self._center = xy
1821 self._path = None
1822 self.stale = True
1823
1824 def get_center(self):
1825 """Return the center of the annulus."""
1826 return self._center
1827
1828 center = property(get_center, set_center)
1829
1830 def set_width(self, width):
1831 """
1832 Set the width (thickness) of the annulus ring.
1833
1834 The width is measured inwards from the outer ellipse.
1835
1836 Parameters
1837 ----------
1838 width : float
1839 """
1840 if width > min(self.a, self.b):
1841 raise ValueError(
1842 'Width of annulus must be less than or equal to semi-minor axis')
1843
1844 self._width = width
1845 self._path = None
1846 self.stale = True
1847
1848 def get_width(self):
1849 """Return the width (thickness) of the annulus ring."""
1850 return self._width
1851
1852 width = property(get_width, set_width)
1853
1854 def set_angle(self, angle):
1855 """
1856 Set the tilt angle of the annulus.
1857
1858 Parameters
1859 ----------
1860 angle : float
1861 """
1862 self._angle = angle
1863 self._path = None
1864 self.stale = True
1865
1866 def get_angle(self):
1867 """Return the angle of the annulus."""
1868 return self._angle
1869
1870 angle = property(get_angle, set_angle)
1871
1872 def set_semimajor(self, a):
1873 """
1874 Set the semi-major axis *a* of the annulus.
1875
1876 Parameters
1877 ----------
1878 a : float
1879 """
1880 self.a = float(a)
1881 self._path = None
1882 self.stale = True
1883
1884 def set_semiminor(self, b):
1885 """
1886 Set the semi-minor axis *b* of the annulus.
1887
1888 Parameters
1889 ----------
1890 b : float
1891 """
1892 self.b = float(b)
1893 self._path = None
1894 self.stale = True
1895
1896 def set_radii(self, r):
1897 """
1898 Set the semi-major (*a*) and semi-minor radii (*b*) of the annulus.
1899
1900 Parameters
1901 ----------
1902 r : float or (float, float)
1903 The radius, or semi-axes:
1904
1905 - If float: radius of the outer circle.
1906 - If two floats: semi-major and -minor axes of outer ellipse.
1907 """
1908 if np.shape(r) == (2,):
1909 self.a, self.b = r
1910 elif np.shape(r) == ():
1911 self.a = self.b = float(r)
1912 else:
1913 raise ValueError("Parameter 'r' must be one or two floats.")
1914
1915 self._path = None
1916 self.stale = True
1917
1918 def get_radii(self):
1919 """Return the semi-major and semi-minor radii of the annulus."""
1920 return self.a, self.b
1921
1922 radii = property(get_radii, set_radii)
1923
1924 def _transform_verts(self, verts, a, b):
1925 return transforms.Affine2D() \
1926 .scale(*self._convert_xy_units((a, b))) \
1927 .rotate_deg(self.angle) \
1928 .translate(*self._convert_xy_units(self.center)) \
1929 .transform(verts)
1930
1931 def _recompute_path(self):
1932 # circular arc
1933 arc = Path.arc(0, 360)
1934
1935 # annulus needs to draw an outer ring
1936 # followed by a reversed and scaled inner ring
1937 a, b, w = self.a, self.b, self.width
1938 v1 = self._transform_verts(arc.vertices, a, b)
1939 v2 = self._transform_verts(arc.vertices[::-1], a - w, b - w)
1940 v = np.vstack([v1, v2, v1[0, :], (0, 0)])
1941 c = np.hstack([arc.codes, Path.MOVETO,
1942 arc.codes[1:], Path.MOVETO,
1943 Path.CLOSEPOLY])
1944 self._path = Path(v, c)
1945
1946 def get_path(self):
1947 if self._path is None:
1948 self._recompute_path()
1949 return self._path
1950
1951
1952class Circle(Ellipse):
1953 """
1954 A circle patch.
1955 """
1956 def __str__(self):
1957 pars = self.center[0], self.center[1], self.radius
1958 fmt = "Circle(xy=(%g, %g), radius=%g)"
1959 return fmt % pars
1960
1961 @_docstring.dedent_interpd
1962 def __init__(self, xy, radius=5, **kwargs):
1963 """
1964 Create a true circle at center *xy* = (*x*, *y*) with given *radius*.
1965
1966 Unlike `CirclePolygon` which is a polygonal approximation, this uses
1967 Bezier splines and is much closer to a scale-free circle.
1968
1969 Valid keyword arguments are:
1970
1971 %(Patch:kwdoc)s
1972 """
1973 super().__init__(xy, radius * 2, radius * 2, **kwargs)
1974 self.radius = radius
1975
1976 def set_radius(self, radius):
1977 """
1978 Set the radius of the circle.
1979
1980 Parameters
1981 ----------
1982 radius : float
1983 """
1984 self.width = self.height = 2 * radius
1985 self.stale = True
1986
1987 def get_radius(self):
1988 """Return the radius of the circle."""
1989 return self.width / 2.
1990
1991 radius = property(get_radius, set_radius)
1992
1993
1994class Arc(Ellipse):
1995 """
1996 An elliptical arc, i.e. a segment of an ellipse.
1997
1998 Due to internal optimizations, the arc cannot be filled.
1999 """
2000
2001 def __str__(self):
2002 pars = (self.center[0], self.center[1], self.width,
2003 self.height, self.angle, self.theta1, self.theta2)
2004 fmt = ("Arc(xy=(%g, %g), width=%g, "
2005 "height=%g, angle=%g, theta1=%g, theta2=%g)")
2006 return fmt % pars
2007
2008 @_docstring.dedent_interpd
2009 def __init__(self, xy, width, height, *,
2010 angle=0.0, theta1=0.0, theta2=360.0, **kwargs):
2011 """
2012 Parameters
2013 ----------
2014 xy : (float, float)
2015 The center of the ellipse.
2016
2017 width : float
2018 The length of the horizontal axis.
2019
2020 height : float
2021 The length of the vertical axis.
2022
2023 angle : float
2024 Rotation of the ellipse in degrees (counterclockwise).
2025
2026 theta1, theta2 : float, default: 0, 360
2027 Starting and ending angles of the arc in degrees. These values
2028 are relative to *angle*, e.g. if *angle* = 45 and *theta1* = 90
2029 the absolute starting angle is 135.
2030 Default *theta1* = 0, *theta2* = 360, i.e. a complete ellipse.
2031 The arc is drawn in the counterclockwise direction.
2032 Angles greater than or equal to 360, or smaller than 0, are
2033 represented by an equivalent angle in the range [0, 360), by
2034 taking the input value mod 360.
2035
2036 Other Parameters
2037 ----------------
2038 **kwargs : `~matplotlib.patches.Patch` properties
2039 Most `.Patch` properties are supported as keyword arguments,
2040 except *fill* and *facecolor* because filling is not supported.
2041
2042 %(Patch:kwdoc)s
2043 """
2044 fill = kwargs.setdefault('fill', False)
2045 if fill:
2046 raise ValueError("Arc objects cannot be filled")
2047
2048 super().__init__(xy, width, height, angle=angle, **kwargs)
2049
2050 self.theta1 = theta1
2051 self.theta2 = theta2
2052 (self._theta1, self._theta2, self._stretched_width,
2053 self._stretched_height) = self._theta_stretch()
2054 self._path = Path.arc(self._theta1, self._theta2)
2055
2056 @artist.allow_rasterization
2057 def draw(self, renderer):
2058 """
2059 Draw the arc to the given *renderer*.
2060
2061 Notes
2062 -----
2063 Ellipses are normally drawn using an approximation that uses
2064 eight cubic Bezier splines. The error of this approximation
2065 is 1.89818e-6, according to this unverified source:
2066
2067 Lancaster, Don. *Approximating a Circle or an Ellipse Using
2068 Four Bezier Cubic Splines.*
2069
2070 https://www.tinaja.com/glib/ellipse4.pdf
2071
2072 There is a use case where very large ellipses must be drawn
2073 with very high accuracy, and it is too expensive to render the
2074 entire ellipse with enough segments (either splines or line
2075 segments). Therefore, in the case where either radius of the
2076 ellipse is large enough that the error of the spline
2077 approximation will be visible (greater than one pixel offset
2078 from the ideal), a different technique is used.
2079
2080 In that case, only the visible parts of the ellipse are drawn,
2081 with each visible arc using a fixed number of spline segments
2082 (8). The algorithm proceeds as follows:
2083
2084 1. The points where the ellipse intersects the axes (or figure)
2085 bounding box are located. (This is done by performing an inverse
2086 transformation on the bbox such that it is relative to the unit
2087 circle -- this makes the intersection calculation much easier than
2088 doing rotated ellipse intersection directly.)
2089
2090 This uses the "line intersecting a circle" algorithm from:
2091
2092 Vince, John. *Geometry for Computer Graphics: Formulae,
2093 Examples & Proofs.* London: Springer-Verlag, 2005.
2094
2095 2. The angles of each of the intersection points are calculated.
2096
2097 3. Proceeding counterclockwise starting in the positive
2098 x-direction, each of the visible arc-segments between the
2099 pairs of vertices are drawn using the Bezier arc
2100 approximation technique implemented in `.Path.arc`.
2101 """
2102 if not self.get_visible():
2103 return
2104
2105 self._recompute_transform()
2106
2107 self._update_path()
2108 # Get width and height in pixels we need to use
2109 # `self.get_data_transform` rather than `self.get_transform`
2110 # because we want the transform from dataspace to the
2111 # screen space to estimate how big the arc will be in physical
2112 # units when rendered (the transform that we get via
2113 # `self.get_transform()` goes from an idealized unit-radius
2114 # space to screen space).
2115 data_to_screen_trans = self.get_data_transform()
2116 pwidth, pheight = (
2117 data_to_screen_trans.transform((self._stretched_width,
2118 self._stretched_height)) -
2119 data_to_screen_trans.transform((0, 0)))
2120 inv_error = (1.0 / 1.89818e-6) * 0.5
2121
2122 if pwidth < inv_error and pheight < inv_error:
2123 return Patch.draw(self, renderer)
2124
2125 def line_circle_intersect(x0, y0, x1, y1):
2126 dx = x1 - x0
2127 dy = y1 - y0
2128 dr2 = dx * dx + dy * dy
2129 D = x0 * y1 - x1 * y0
2130 D2 = D * D
2131 discrim = dr2 - D2
2132 if discrim >= 0.0:
2133 sign_dy = np.copysign(1, dy) # +/-1, never 0.
2134 sqrt_discrim = np.sqrt(discrim)
2135 return np.array(
2136 [[(D * dy + sign_dy * dx * sqrt_discrim) / dr2,
2137 (-D * dx + abs(dy) * sqrt_discrim) / dr2],
2138 [(D * dy - sign_dy * dx * sqrt_discrim) / dr2,
2139 (-D * dx - abs(dy) * sqrt_discrim) / dr2]])
2140 else:
2141 return np.empty((0, 2))
2142
2143 def segment_circle_intersect(x0, y0, x1, y1):
2144 epsilon = 1e-9
2145 if x1 < x0:
2146 x0e, x1e = x1, x0
2147 else:
2148 x0e, x1e = x0, x1
2149 if y1 < y0:
2150 y0e, y1e = y1, y0
2151 else:
2152 y0e, y1e = y0, y1
2153 xys = line_circle_intersect(x0, y0, x1, y1)
2154 xs, ys = xys.T
2155 return xys[
2156 (x0e - epsilon < xs) & (xs < x1e + epsilon)
2157 & (y0e - epsilon < ys) & (ys < y1e + epsilon)
2158 ]
2159
2160 # Transform the Axes (or figure) box_path so that it is relative to
2161 # the unit circle in the same way that it is relative to the desired
2162 # ellipse.
2163 box_path_transform = (
2164 transforms.BboxTransformTo((self.axes or self.figure).bbox)
2165 - self.get_transform())
2166 box_path = Path.unit_rectangle().transformed(box_path_transform)
2167
2168 thetas = set()
2169 # For each of the point pairs, there is a line segment
2170 for p0, p1 in zip(box_path.vertices[:-1], box_path.vertices[1:]):
2171 xy = segment_circle_intersect(*p0, *p1)
2172 x, y = xy.T
2173 # arctan2 return [-pi, pi), the rest of our angles are in
2174 # [0, 360], adjust as needed.
2175 theta = (np.rad2deg(np.arctan2(y, x)) + 360) % 360
2176 thetas.update(
2177 theta[(self._theta1 < theta) & (theta < self._theta2)])
2178 thetas = sorted(thetas) + [self._theta2]
2179 last_theta = self._theta1
2180 theta1_rad = np.deg2rad(self._theta1)
2181 inside = box_path.contains_point(
2182 (np.cos(theta1_rad), np.sin(theta1_rad))
2183 )
2184
2185 # save original path
2186 path_original = self._path
2187 for theta in thetas:
2188 if inside:
2189 self._path = Path.arc(last_theta, theta, 8)
2190 Patch.draw(self, renderer)
2191 inside = False
2192 else:
2193 inside = True
2194 last_theta = theta
2195
2196 # restore original path
2197 self._path = path_original
2198
2199 def _update_path(self):
2200 # Compute new values and update and set new _path if any value changed
2201 stretched = self._theta_stretch()
2202 if any(a != b for a, b in zip(
2203 stretched, (self._theta1, self._theta2, self._stretched_width,
2204 self._stretched_height))):
2205 (self._theta1, self._theta2, self._stretched_width,
2206 self._stretched_height) = stretched
2207 self._path = Path.arc(self._theta1, self._theta2)
2208
2209 def _theta_stretch(self):
2210 # If the width and height of ellipse are not equal, take into account
2211 # stretching when calculating angles to draw between
2212 def theta_stretch(theta, scale):
2213 theta = np.deg2rad(theta)
2214 x = np.cos(theta)
2215 y = np.sin(theta)
2216 stheta = np.rad2deg(np.arctan2(scale * y, x))
2217 # arctan2 has the range [-pi, pi], we expect [0, 2*pi]
2218 return (stheta + 360) % 360
2219
2220 width = self.convert_xunits(self.width)
2221 height = self.convert_yunits(self.height)
2222 if (
2223 # if we need to stretch the angles because we are distorted
2224 width != height
2225 # and we are not doing a full circle.
2226 #
2227 # 0 and 360 do not exactly round-trip through the angle
2228 # stretching (due to both float precision limitations and
2229 # the difference between the range of arctan2 [-pi, pi] and
2230 # this method [0, 360]) so avoid doing it if we don't have to.
2231 and not (self.theta1 != self.theta2 and
2232 self.theta1 % 360 == self.theta2 % 360)
2233 ):
2234 theta1 = theta_stretch(self.theta1, width / height)
2235 theta2 = theta_stretch(self.theta2, width / height)
2236 return theta1, theta2, width, height
2237 return self.theta1, self.theta2, width, height
2238
2239
2240def bbox_artist(artist, renderer, props=None, fill=True):
2241 """
2242 A debug function to draw a rectangle around the bounding
2243 box returned by an artist's `.Artist.get_window_extent`
2244 to test whether the artist is returning the correct bbox.
2245
2246 *props* is a dict of rectangle props with the additional property
2247 'pad' that sets the padding around the bbox in points.
2248 """
2249 if props is None:
2250 props = {}
2251 props = props.copy() # don't want to alter the pad externally
2252 pad = props.pop('pad', 4)
2253 pad = renderer.points_to_pixels(pad)
2254 bbox = artist.get_window_extent(renderer)
2255 r = Rectangle(
2256 xy=(bbox.x0 - pad / 2, bbox.y0 - pad / 2),
2257 width=bbox.width + pad, height=bbox.height + pad,
2258 fill=fill, transform=transforms.IdentityTransform(), clip_on=False)
2259 r.update(props)
2260 r.draw(renderer)
2261
2262
2263def draw_bbox(bbox, renderer, color='k', trans=None):
2264 """
2265 A debug function to draw a rectangle around the bounding
2266 box returned by an artist's `.Artist.get_window_extent`
2267 to test whether the artist is returning the correct bbox.
2268 """
2269 r = Rectangle(xy=bbox.p0, width=bbox.width, height=bbox.height,
2270 edgecolor=color, fill=False, clip_on=False)
2271 if trans is not None:
2272 r.set_transform(trans)
2273 r.draw(renderer)
2274
2275
2276class _Style:
2277 """
2278 A base class for the Styles. It is meant to be a container class,
2279 where actual styles are declared as subclass of it, and it
2280 provides some helper functions.
2281 """
2282
2283 def __init_subclass__(cls):
2284 # Automatically perform docstring interpolation on the subclasses:
2285 # This allows listing the supported styles via
2286 # - %(BoxStyle:table)s
2287 # - %(ConnectionStyle:table)s
2288 # - %(ArrowStyle:table)s
2289 # and additionally adding .. ACCEPTS: blocks via
2290 # - %(BoxStyle:table_and_accepts)s
2291 # - %(ConnectionStyle:table_and_accepts)s
2292 # - %(ArrowStyle:table_and_accepts)s
2293 _docstring.interpd.update({
2294 f"{cls.__name__}:table": cls.pprint_styles(),
2295 f"{cls.__name__}:table_and_accepts": (
2296 cls.pprint_styles()
2297 + "\n\n .. ACCEPTS: ["
2298 + "|".join(map(" '{}' ".format, cls._style_list))
2299 + "]")
2300 })
2301
2302 def __new__(cls, stylename, **kwargs):
2303 """Return the instance of the subclass with the given style name."""
2304 # The "class" should have the _style_list attribute, which is a mapping
2305 # of style names to style classes.
2306 _list = stylename.replace(" ", "").split(",")
2307 _name = _list[0].lower()
2308 try:
2309 _cls = cls._style_list[_name]
2310 except KeyError as err:
2311 raise ValueError(f"Unknown style: {stylename!r}") from err
2312 try:
2313 _args_pair = [cs.split("=") for cs in _list[1:]]
2314 _args = {k: float(v) for k, v in _args_pair}
2315 except ValueError as err:
2316 raise ValueError(
2317 f"Incorrect style argument: {stylename!r}") from err
2318 return _cls(**{**_args, **kwargs})
2319
2320 @classmethod
2321 def get_styles(cls):
2322 """Return a dictionary of available styles."""
2323 return cls._style_list
2324
2325 @classmethod
2326 def pprint_styles(cls):
2327 """Return the available styles as pretty-printed string."""
2328 table = [('Class', 'Name', 'Attrs'),
2329 *[(cls.__name__,
2330 # Add backquotes, as - and | have special meaning in reST.
2331 f'``{name}``',
2332 # [1:-1] drops the surrounding parentheses.
2333 str(inspect.signature(cls))[1:-1] or 'None')
2334 for name, cls in cls._style_list.items()]]
2335 # Convert to rst table.
2336 col_len = [max(len(cell) for cell in column) for column in zip(*table)]
2337 table_formatstr = ' '.join('=' * cl for cl in col_len)
2338 rst_table = '\n'.join([
2339 '',
2340 table_formatstr,
2341 ' '.join(cell.ljust(cl) for cell, cl in zip(table[0], col_len)),
2342 table_formatstr,
2343 *[' '.join(cell.ljust(cl) for cell, cl in zip(row, col_len))
2344 for row in table[1:]],
2345 table_formatstr,
2346 ])
2347 return textwrap.indent(rst_table, prefix=' ' * 4)
2348
2349 @classmethod
2350 def register(cls, name, style):
2351 """Register a new style."""
2352 if not issubclass(style, cls._Base):
2353 raise ValueError(f"{style} must be a subclass of {cls._Base}")
2354 cls._style_list[name] = style
2355
2356
2357def _register_style(style_list, cls=None, *, name=None):
2358 """Class decorator that stashes a class in a (style) dictionary."""
2359 if cls is None:
2360 return functools.partial(_register_style, style_list, name=name)
2361 style_list[name or cls.__name__.lower()] = cls
2362 return cls
2363
2364
2365@_docstring.dedent_interpd
2366class BoxStyle(_Style):
2367 """
2368 `BoxStyle` is a container class which defines several
2369 boxstyle classes, which are used for `FancyBboxPatch`.
2370
2371 A style object can be created as::
2372
2373 BoxStyle.Round(pad=0.2)
2374
2375 or::
2376
2377 BoxStyle("Round", pad=0.2)
2378
2379 or::
2380
2381 BoxStyle("Round, pad=0.2")
2382
2383 The following boxstyle classes are defined.
2384
2385 %(BoxStyle:table)s
2386
2387 An instance of a boxstyle class is a callable object, with the signature ::
2388
2389 __call__(self, x0, y0, width, height, mutation_size) -> Path
2390
2391 *x0*, *y0*, *width* and *height* specify the location and size of the box
2392 to be drawn; *mutation_size* scales the outline properties such as padding.
2393 """
2394
2395 _style_list = {}
2396
2397 @_register_style(_style_list)
2398 class Square:
2399 """A square box."""
2400
2401 def __init__(self, pad=0.3):
2402 """
2403 Parameters
2404 ----------
2405 pad : float, default: 0.3
2406 The amount of padding around the original box.
2407 """
2408 self.pad = pad
2409
2410 def __call__(self, x0, y0, width, height, mutation_size):
2411 pad = mutation_size * self.pad
2412 # width and height with padding added.
2413 width, height = width + 2 * pad, height + 2 * pad
2414 # boundary of the padded box
2415 x0, y0 = x0 - pad, y0 - pad
2416 x1, y1 = x0 + width, y0 + height
2417 return Path._create_closed(
2418 [(x0, y0), (x1, y0), (x1, y1), (x0, y1)])
2419
2420 @_register_style(_style_list)
2421 class Circle:
2422 """A circular box."""
2423
2424 def __init__(self, pad=0.3):
2425 """
2426 Parameters
2427 ----------
2428 pad : float, default: 0.3
2429 The amount of padding around the original box.
2430 """
2431 self.pad = pad
2432
2433 def __call__(self, x0, y0, width, height, mutation_size):
2434 pad = mutation_size * self.pad
2435 width, height = width + 2 * pad, height + 2 * pad
2436 # boundary of the padded box
2437 x0, y0 = x0 - pad, y0 - pad
2438 return Path.circle((x0 + width / 2, y0 + height / 2),
2439 max(width, height) / 2)
2440
2441 @_register_style(_style_list)
2442 class Ellipse:
2443 """
2444 An elliptical box.
2445
2446 .. versionadded:: 3.7
2447 """
2448
2449 def __init__(self, pad=0.3):
2450 """
2451 Parameters
2452 ----------
2453 pad : float, default: 0.3
2454 The amount of padding around the original box.
2455 """
2456 self.pad = pad
2457
2458 def __call__(self, x0, y0, width, height, mutation_size):
2459 pad = mutation_size * self.pad
2460 width, height = width + 2 * pad, height + 2 * pad
2461 # boundary of the padded box
2462 x0, y0 = x0 - pad, y0 - pad
2463 a = width / math.sqrt(2)
2464 b = height / math.sqrt(2)
2465 trans = Affine2D().scale(a, b).translate(x0 + width / 2,
2466 y0 + height / 2)
2467 return trans.transform_path(Path.unit_circle())
2468
2469 @_register_style(_style_list)
2470 class LArrow:
2471 """A box in the shape of a left-pointing arrow."""
2472
2473 def __init__(self, pad=0.3):
2474 """
2475 Parameters
2476 ----------
2477 pad : float, default: 0.3
2478 The amount of padding around the original box.
2479 """
2480 self.pad = pad
2481
2482 def __call__(self, x0, y0, width, height, mutation_size):
2483 # padding
2484 pad = mutation_size * self.pad
2485 # width and height with padding added.
2486 width, height = width + 2 * pad, height + 2 * pad
2487 # boundary of the padded box
2488 x0, y0 = x0 - pad, y0 - pad,
2489 x1, y1 = x0 + width, y0 + height
2490
2491 dx = (y1 - y0) / 2
2492 dxx = dx / 2
2493 x0 = x0 + pad / 1.4 # adjust by ~sqrt(2)
2494
2495 return Path._create_closed(
2496 [(x0 + dxx, y0), (x1, y0), (x1, y1), (x0 + dxx, y1),
2497 (x0 + dxx, y1 + dxx), (x0 - dx, y0 + dx),
2498 (x0 + dxx, y0 - dxx), # arrow
2499 (x0 + dxx, y0)])
2500
2501 @_register_style(_style_list)
2502 class RArrow(LArrow):
2503 """A box in the shape of a right-pointing arrow."""
2504
2505 def __call__(self, x0, y0, width, height, mutation_size):
2506 p = BoxStyle.LArrow.__call__(
2507 self, x0, y0, width, height, mutation_size)
2508 p.vertices[:, 0] = 2 * x0 + width - p.vertices[:, 0]
2509 return p
2510
2511 @_register_style(_style_list)
2512 class DArrow:
2513 """A box in the shape of a two-way arrow."""
2514 # Modified from LArrow to add a right arrow to the bbox.
2515
2516 def __init__(self, pad=0.3):
2517 """
2518 Parameters
2519 ----------
2520 pad : float, default: 0.3
2521 The amount of padding around the original box.
2522 """
2523 self.pad = pad
2524
2525 def __call__(self, x0, y0, width, height, mutation_size):
2526 # padding
2527 pad = mutation_size * self.pad
2528 # width and height with padding added.
2529 # The width is padded by the arrows, so we don't need to pad it.
2530 height = height + 2 * pad
2531 # boundary of the padded box
2532 x0, y0 = x0 - pad, y0 - pad
2533 x1, y1 = x0 + width, y0 + height
2534
2535 dx = (y1 - y0) / 2
2536 dxx = dx / 2
2537 x0 = x0 + pad / 1.4 # adjust by ~sqrt(2)
2538
2539 return Path._create_closed([
2540 (x0 + dxx, y0), (x1, y0), # bot-segment
2541 (x1, y0 - dxx), (x1 + dx + dxx, y0 + dx),
2542 (x1, y1 + dxx), # right-arrow
2543 (x1, y1), (x0 + dxx, y1), # top-segment
2544 (x0 + dxx, y1 + dxx), (x0 - dx, y0 + dx),
2545 (x0 + dxx, y0 - dxx), # left-arrow
2546 (x0 + dxx, y0)])
2547
2548 @_register_style(_style_list)
2549 class Round:
2550 """A box with round corners."""
2551
2552 def __init__(self, pad=0.3, rounding_size=None):
2553 """
2554 Parameters
2555 ----------
2556 pad : float, default: 0.3
2557 The amount of padding around the original box.
2558 rounding_size : float, default: *pad*
2559 Radius of the corners.
2560 """
2561 self.pad = pad
2562 self.rounding_size = rounding_size
2563
2564 def __call__(self, x0, y0, width, height, mutation_size):
2565
2566 # padding
2567 pad = mutation_size * self.pad
2568
2569 # size of the rounding corner
2570 if self.rounding_size:
2571 dr = mutation_size * self.rounding_size
2572 else:
2573 dr = pad
2574
2575 width, height = width + 2 * pad, height + 2 * pad
2576
2577 x0, y0 = x0 - pad, y0 - pad,
2578 x1, y1 = x0 + width, y0 + height
2579
2580 # Round corners are implemented as quadratic Bezier, e.g.,
2581 # [(x0, y0-dr), (x0, y0), (x0+dr, y0)] for lower left corner.
2582 cp = [(x0 + dr, y0),
2583 (x1 - dr, y0),
2584 (x1, y0), (x1, y0 + dr),
2585 (x1, y1 - dr),
2586 (x1, y1), (x1 - dr, y1),
2587 (x0 + dr, y1),
2588 (x0, y1), (x0, y1 - dr),
2589 (x0, y0 + dr),
2590 (x0, y0), (x0 + dr, y0),
2591 (x0 + dr, y0)]
2592
2593 com = [Path.MOVETO,
2594 Path.LINETO,
2595 Path.CURVE3, Path.CURVE3,
2596 Path.LINETO,
2597 Path.CURVE3, Path.CURVE3,
2598 Path.LINETO,
2599 Path.CURVE3, Path.CURVE3,
2600 Path.LINETO,
2601 Path.CURVE3, Path.CURVE3,
2602 Path.CLOSEPOLY]
2603
2604 return Path(cp, com)
2605
2606 @_register_style(_style_list)
2607 class Round4:
2608 """A box with rounded edges."""
2609
2610 def __init__(self, pad=0.3, rounding_size=None):
2611 """
2612 Parameters
2613 ----------
2614 pad : float, default: 0.3
2615 The amount of padding around the original box.
2616 rounding_size : float, default: *pad*/2
2617 Rounding of edges.
2618 """
2619 self.pad = pad
2620 self.rounding_size = rounding_size
2621
2622 def __call__(self, x0, y0, width, height, mutation_size):
2623
2624 # padding
2625 pad = mutation_size * self.pad
2626
2627 # Rounding size; defaults to half of the padding.
2628 if self.rounding_size:
2629 dr = mutation_size * self.rounding_size
2630 else:
2631 dr = pad / 2.
2632
2633 width = width + 2 * pad - 2 * dr
2634 height = height + 2 * pad - 2 * dr
2635
2636 x0, y0 = x0 - pad + dr, y0 - pad + dr,
2637 x1, y1 = x0 + width, y0 + height
2638
2639 cp = [(x0, y0),
2640 (x0 + dr, y0 - dr), (x1 - dr, y0 - dr), (x1, y0),
2641 (x1 + dr, y0 + dr), (x1 + dr, y1 - dr), (x1, y1),
2642 (x1 - dr, y1 + dr), (x0 + dr, y1 + dr), (x0, y1),
2643 (x0 - dr, y1 - dr), (x0 - dr, y0 + dr), (x0, y0),
2644 (x0, y0)]
2645
2646 com = [Path.MOVETO,
2647 Path.CURVE4, Path.CURVE4, Path.CURVE4,
2648 Path.CURVE4, Path.CURVE4, Path.CURVE4,
2649 Path.CURVE4, Path.CURVE4, Path.CURVE4,
2650 Path.CURVE4, Path.CURVE4, Path.CURVE4,
2651 Path.CLOSEPOLY]
2652
2653 return Path(cp, com)
2654
2655 @_register_style(_style_list)
2656 class Sawtooth:
2657 """A box with a sawtooth outline."""
2658
2659 def __init__(self, pad=0.3, tooth_size=None):
2660 """
2661 Parameters
2662 ----------
2663 pad : float, default: 0.3
2664 The amount of padding around the original box.
2665 tooth_size : float, default: *pad*/2
2666 Size of the sawtooth.
2667 """
2668 self.pad = pad
2669 self.tooth_size = tooth_size
2670
2671 def _get_sawtooth_vertices(self, x0, y0, width, height, mutation_size):
2672
2673 # padding
2674 pad = mutation_size * self.pad
2675
2676 # size of sawtooth
2677 if self.tooth_size is None:
2678 tooth_size = self.pad * .5 * mutation_size
2679 else:
2680 tooth_size = self.tooth_size * mutation_size
2681
2682 hsz = tooth_size / 2
2683 width = width + 2 * pad - tooth_size
2684 height = height + 2 * pad - tooth_size
2685
2686 # the sizes of the vertical and horizontal sawtooth are
2687 # separately adjusted to fit the given box size.
2688 dsx_n = round((width - tooth_size) / (tooth_size * 2)) * 2
2689 dsy_n = round((height - tooth_size) / (tooth_size * 2)) * 2
2690
2691 x0, y0 = x0 - pad + hsz, y0 - pad + hsz
2692 x1, y1 = x0 + width, y0 + height
2693
2694 xs = [
2695 x0, *np.linspace(x0 + hsz, x1 - hsz, 2 * dsx_n + 1), # bottom
2696 *([x1, x1 + hsz, x1, x1 - hsz] * dsy_n)[:2*dsy_n+2], # right
2697 x1, *np.linspace(x1 - hsz, x0 + hsz, 2 * dsx_n + 1), # top
2698 *([x0, x0 - hsz, x0, x0 + hsz] * dsy_n)[:2*dsy_n+2], # left
2699 ]
2700 ys = [
2701 *([y0, y0 - hsz, y0, y0 + hsz] * dsx_n)[:2*dsx_n+2], # bottom
2702 y0, *np.linspace(y0 + hsz, y1 - hsz, 2 * dsy_n + 1), # right
2703 *([y1, y1 + hsz, y1, y1 - hsz] * dsx_n)[:2*dsx_n+2], # top
2704 y1, *np.linspace(y1 - hsz, y0 + hsz, 2 * dsy_n + 1), # left
2705 ]
2706
2707 return [*zip(xs, ys), (xs[0], ys[0])]
2708
2709 def __call__(self, x0, y0, width, height, mutation_size):
2710 saw_vertices = self._get_sawtooth_vertices(x0, y0, width,
2711 height, mutation_size)
2712 return Path(saw_vertices, closed=True)
2713
2714 @_register_style(_style_list)
2715 class Roundtooth(Sawtooth):
2716 """A box with a rounded sawtooth outline."""
2717
2718 def __call__(self, x0, y0, width, height, mutation_size):
2719 saw_vertices = self._get_sawtooth_vertices(x0, y0,
2720 width, height,
2721 mutation_size)
2722 # Add a trailing vertex to allow us to close the polygon correctly
2723 saw_vertices = np.concatenate([saw_vertices, [saw_vertices[0]]])
2724 codes = ([Path.MOVETO] +
2725 [Path.CURVE3, Path.CURVE3] * ((len(saw_vertices)-1)//2) +
2726 [Path.CLOSEPOLY])
2727 return Path(saw_vertices, codes)
2728
2729
2730@_docstring.dedent_interpd
2731class ConnectionStyle(_Style):
2732 """
2733 `ConnectionStyle` is a container class which defines
2734 several connectionstyle classes, which is used to create a path
2735 between two points. These are mainly used with `FancyArrowPatch`.
2736
2737 A connectionstyle object can be either created as::
2738
2739 ConnectionStyle.Arc3(rad=0.2)
2740
2741 or::
2742
2743 ConnectionStyle("Arc3", rad=0.2)
2744
2745 or::
2746
2747 ConnectionStyle("Arc3, rad=0.2")
2748
2749 The following classes are defined
2750
2751 %(ConnectionStyle:table)s
2752
2753 An instance of any connection style class is a callable object,
2754 whose call signature is::
2755
2756 __call__(self, posA, posB,
2757 patchA=None, patchB=None,
2758 shrinkA=2., shrinkB=2.)
2759
2760 and it returns a `.Path` instance. *posA* and *posB* are
2761 tuples of (x, y) coordinates of the two points to be
2762 connected. *patchA* (or *patchB*) is given, the returned path is
2763 clipped so that it start (or end) from the boundary of the
2764 patch. The path is further shrunk by *shrinkA* (or *shrinkB*)
2765 which is given in points.
2766 """
2767
2768 _style_list = {}
2769
2770 class _Base:
2771 """
2772 A base class for connectionstyle classes. The subclass needs
2773 to implement a *connect* method whose call signature is::
2774
2775 connect(posA, posB)
2776
2777 where posA and posB are tuples of x, y coordinates to be
2778 connected. The method needs to return a path connecting two
2779 points. This base class defines a __call__ method, and a few
2780 helper methods.
2781 """
2782 def _in_patch(self, patch):
2783 """
2784 Return a predicate function testing whether a point *xy* is
2785 contained in *patch*.
2786 """
2787 return lambda xy: patch.contains(
2788 SimpleNamespace(x=xy[0], y=xy[1]))[0]
2789
2790 def _clip(self, path, in_start, in_stop):
2791 """
2792 Clip *path* at its start by the region where *in_start* returns
2793 True, and at its stop by the region where *in_stop* returns True.
2794
2795 The original path is assumed to start in the *in_start* region and
2796 to stop in the *in_stop* region.
2797 """
2798 if in_start:
2799 try:
2800 _, path = split_path_inout(path, in_start)
2801 except ValueError:
2802 pass
2803 if in_stop:
2804 try:
2805 path, _ = split_path_inout(path, in_stop)
2806 except ValueError:
2807 pass
2808 return path
2809
2810 def __call__(self, posA, posB,
2811 shrinkA=2., shrinkB=2., patchA=None, patchB=None):
2812 """
2813 Call the *connect* method to create a path between *posA* and
2814 *posB*; then clip and shrink the path.
2815 """
2816 path = self.connect(posA, posB)
2817 path = self._clip(
2818 path,
2819 self._in_patch(patchA) if patchA else None,
2820 self._in_patch(patchB) if patchB else None,
2821 )
2822 path = self._clip(
2823 path,
2824 inside_circle(*path.vertices[0], shrinkA) if shrinkA else None,
2825 inside_circle(*path.vertices[-1], shrinkB) if shrinkB else None
2826 )
2827 return path
2828
2829 @_register_style(_style_list)
2830 class Arc3(_Base):
2831 """
2832 Creates a simple quadratic Bézier curve between two
2833 points. The curve is created so that the middle control point
2834 (C1) is located at the same distance from the start (C0) and
2835 end points(C2) and the distance of the C1 to the line
2836 connecting C0-C2 is *rad* times the distance of C0-C2.
2837 """
2838
2839 def __init__(self, rad=0.):
2840 """
2841 Parameters
2842 ----------
2843 rad : float
2844 Curvature of the curve.
2845 """
2846 self.rad = rad
2847
2848 def connect(self, posA, posB):
2849 x1, y1 = posA
2850 x2, y2 = posB
2851 x12, y12 = (x1 + x2) / 2., (y1 + y2) / 2.
2852 dx, dy = x2 - x1, y2 - y1
2853
2854 f = self.rad
2855
2856 cx, cy = x12 + f * dy, y12 - f * dx
2857
2858 vertices = [(x1, y1),
2859 (cx, cy),
2860 (x2, y2)]
2861 codes = [Path.MOVETO,
2862 Path.CURVE3,
2863 Path.CURVE3]
2864
2865 return Path(vertices, codes)
2866
2867 @_register_style(_style_list)
2868 class Angle3(_Base):
2869 """
2870 Creates a simple quadratic Bézier curve between two points. The middle
2871 control point is placed at the intersecting point of two lines which
2872 cross the start and end point, and have a slope of *angleA* and
2873 *angleB*, respectively.
2874 """
2875
2876 def __init__(self, angleA=90, angleB=0):
2877 """
2878 Parameters
2879 ----------
2880 angleA : float
2881 Starting angle of the path.
2882
2883 angleB : float
2884 Ending angle of the path.
2885 """
2886
2887 self.angleA = angleA
2888 self.angleB = angleB
2889
2890 def connect(self, posA, posB):
2891 x1, y1 = posA
2892 x2, y2 = posB
2893
2894 cosA = math.cos(math.radians(self.angleA))
2895 sinA = math.sin(math.radians(self.angleA))
2896 cosB = math.cos(math.radians(self.angleB))
2897 sinB = math.sin(math.radians(self.angleB))
2898
2899 cx, cy = get_intersection(x1, y1, cosA, sinA,
2900 x2, y2, cosB, sinB)
2901
2902 vertices = [(x1, y1), (cx, cy), (x2, y2)]
2903 codes = [Path.MOVETO, Path.CURVE3, Path.CURVE3]
2904
2905 return Path(vertices, codes)
2906
2907 @_register_style(_style_list)
2908 class Angle(_Base):
2909 """
2910 Creates a piecewise continuous quadratic Bézier path between two
2911 points. The path has a one passing-through point placed at the
2912 intersecting point of two lines which cross the start and end point,
2913 and have a slope of *angleA* and *angleB*, respectively.
2914 The connecting edges are rounded with *rad*.
2915 """
2916
2917 def __init__(self, angleA=90, angleB=0, rad=0.):
2918 """
2919 Parameters
2920 ----------
2921 angleA : float
2922 Starting angle of the path.
2923
2924 angleB : float
2925 Ending angle of the path.
2926
2927 rad : float
2928 Rounding radius of the edge.
2929 """
2930
2931 self.angleA = angleA
2932 self.angleB = angleB
2933
2934 self.rad = rad
2935
2936 def connect(self, posA, posB):
2937 x1, y1 = posA
2938 x2, y2 = posB
2939
2940 cosA = math.cos(math.radians(self.angleA))
2941 sinA = math.sin(math.radians(self.angleA))
2942 cosB = math.cos(math.radians(self.angleB))
2943 sinB = math.sin(math.radians(self.angleB))
2944
2945 cx, cy = get_intersection(x1, y1, cosA, sinA,
2946 x2, y2, cosB, sinB)
2947
2948 vertices = [(x1, y1)]
2949 codes = [Path.MOVETO]
2950
2951 if self.rad == 0.:
2952 vertices.append((cx, cy))
2953 codes.append(Path.LINETO)
2954 else:
2955 dx1, dy1 = x1 - cx, y1 - cy
2956 d1 = np.hypot(dx1, dy1)
2957 f1 = self.rad / d1
2958 dx2, dy2 = x2 - cx, y2 - cy
2959 d2 = np.hypot(dx2, dy2)
2960 f2 = self.rad / d2
2961 vertices.extend([(cx + dx1 * f1, cy + dy1 * f1),
2962 (cx, cy),
2963 (cx + dx2 * f2, cy + dy2 * f2)])
2964 codes.extend([Path.LINETO, Path.CURVE3, Path.CURVE3])
2965
2966 vertices.append((x2, y2))
2967 codes.append(Path.LINETO)
2968
2969 return Path(vertices, codes)
2970
2971 @_register_style(_style_list)
2972 class Arc(_Base):
2973 """
2974 Creates a piecewise continuous quadratic Bézier path between two
2975 points. The path can have two passing-through points, a
2976 point placed at the distance of *armA* and angle of *angleA* from
2977 point A, another point with respect to point B. The edges are
2978 rounded with *rad*.
2979 """
2980
2981 def __init__(self, angleA=0, angleB=0, armA=None, armB=None, rad=0.):
2982 """
2983 Parameters
2984 ----------
2985 angleA : float
2986 Starting angle of the path.
2987
2988 angleB : float
2989 Ending angle of the path.
2990
2991 armA : float or None
2992 Length of the starting arm.
2993
2994 armB : float or None
2995 Length of the ending arm.
2996
2997 rad : float
2998 Rounding radius of the edges.
2999 """
3000
3001 self.angleA = angleA
3002 self.angleB = angleB
3003 self.armA = armA
3004 self.armB = armB
3005
3006 self.rad = rad
3007
3008 def connect(self, posA, posB):
3009 x1, y1 = posA
3010 x2, y2 = posB
3011
3012 vertices = [(x1, y1)]
3013 rounded = []
3014 codes = [Path.MOVETO]
3015
3016 if self.armA:
3017 cosA = math.cos(math.radians(self.angleA))
3018 sinA = math.sin(math.radians(self.angleA))
3019 # x_armA, y_armB
3020 d = self.armA - self.rad
3021 rounded.append((x1 + d * cosA, y1 + d * sinA))
3022 d = self.armA
3023 rounded.append((x1 + d * cosA, y1 + d * sinA))
3024
3025 if self.armB:
3026 cosB = math.cos(math.radians(self.angleB))
3027 sinB = math.sin(math.radians(self.angleB))
3028 x_armB, y_armB = x2 + self.armB * cosB, y2 + self.armB * sinB
3029
3030 if rounded:
3031 xp, yp = rounded[-1]
3032 dx, dy = x_armB - xp, y_armB - yp
3033 dd = (dx * dx + dy * dy) ** .5
3034
3035 rounded.append((xp + self.rad * dx / dd,
3036 yp + self.rad * dy / dd))
3037 vertices.extend(rounded)
3038 codes.extend([Path.LINETO,
3039 Path.CURVE3,
3040 Path.CURVE3])
3041 else:
3042 xp, yp = vertices[-1]
3043 dx, dy = x_armB - xp, y_armB - yp
3044 dd = (dx * dx + dy * dy) ** .5
3045
3046 d = dd - self.rad
3047 rounded = [(xp + d * dx / dd, yp + d * dy / dd),
3048 (x_armB, y_armB)]
3049
3050 if rounded:
3051 xp, yp = rounded[-1]
3052 dx, dy = x2 - xp, y2 - yp
3053 dd = (dx * dx + dy * dy) ** .5
3054
3055 rounded.append((xp + self.rad * dx / dd,
3056 yp + self.rad * dy / dd))
3057 vertices.extend(rounded)
3058 codes.extend([Path.LINETO,
3059 Path.CURVE3,
3060 Path.CURVE3])
3061
3062 vertices.append((x2, y2))
3063 codes.append(Path.LINETO)
3064
3065 return Path(vertices, codes)
3066
3067 @_register_style(_style_list)
3068 class Bar(_Base):
3069 """
3070 A line with *angle* between A and B with *armA* and *armB*. One of the
3071 arms is extended so that they are connected in a right angle. The
3072 length of *armA* is determined by (*armA* + *fraction* x AB distance).
3073 Same for *armB*.
3074 """
3075
3076 def __init__(self, armA=0., armB=0., fraction=0.3, angle=None):
3077 """
3078 Parameters
3079 ----------
3080 armA : float
3081 Minimum length of armA.
3082
3083 armB : float
3084 Minimum length of armB.
3085
3086 fraction : float
3087 A fraction of the distance between two points that will be
3088 added to armA and armB.
3089
3090 angle : float or None
3091 Angle of the connecting line (if None, parallel to A and B).
3092 """
3093 self.armA = armA
3094 self.armB = armB
3095 self.fraction = fraction
3096 self.angle = angle
3097
3098 def connect(self, posA, posB):
3099 x1, y1 = posA
3100 x20, y20 = x2, y2 = posB
3101
3102 theta1 = math.atan2(y2 - y1, x2 - x1)
3103 dx, dy = x2 - x1, y2 - y1
3104 dd = (dx * dx + dy * dy) ** .5
3105 ddx, ddy = dx / dd, dy / dd
3106
3107 armA, armB = self.armA, self.armB
3108
3109 if self.angle is not None:
3110 theta0 = np.deg2rad(self.angle)
3111 dtheta = theta1 - theta0
3112 dl = dd * math.sin(dtheta)
3113 dL = dd * math.cos(dtheta)
3114 x2, y2 = x1 + dL * math.cos(theta0), y1 + dL * math.sin(theta0)
3115 armB = armB - dl
3116
3117 # update
3118 dx, dy = x2 - x1, y2 - y1
3119 dd2 = (dx * dx + dy * dy) ** .5
3120 ddx, ddy = dx / dd2, dy / dd2
3121
3122 arm = max(armA, armB)
3123 f = self.fraction * dd + arm
3124
3125 cx1, cy1 = x1 + f * ddy, y1 - f * ddx
3126 cx2, cy2 = x2 + f * ddy, y2 - f * ddx
3127
3128 vertices = [(x1, y1),
3129 (cx1, cy1),
3130 (cx2, cy2),
3131 (x20, y20)]
3132 codes = [Path.MOVETO,
3133 Path.LINETO,
3134 Path.LINETO,
3135 Path.LINETO]
3136
3137 return Path(vertices, codes)
3138
3139
3140def _point_along_a_line(x0, y0, x1, y1, d):
3141 """
3142 Return the point on the line connecting (*x0*, *y0*) -- (*x1*, *y1*) whose
3143 distance from (*x0*, *y0*) is *d*.
3144 """
3145 dx, dy = x0 - x1, y0 - y1
3146 ff = d / (dx * dx + dy * dy) ** .5
3147 x2, y2 = x0 - ff * dx, y0 - ff * dy
3148
3149 return x2, y2
3150
3151
3152@_docstring.dedent_interpd
3153class ArrowStyle(_Style):
3154 """
3155 `ArrowStyle` is a container class which defines several
3156 arrowstyle classes, which is used to create an arrow path along a
3157 given path. These are mainly used with `FancyArrowPatch`.
3158
3159 An arrowstyle object can be either created as::
3160
3161 ArrowStyle.Fancy(head_length=.4, head_width=.4, tail_width=.4)
3162
3163 or::
3164
3165 ArrowStyle("Fancy", head_length=.4, head_width=.4, tail_width=.4)
3166
3167 or::
3168
3169 ArrowStyle("Fancy, head_length=.4, head_width=.4, tail_width=.4")
3170
3171 The following classes are defined
3172
3173 %(ArrowStyle:table)s
3174
3175 For an overview of the visual appearance, see
3176 :doc:`/gallery/text_labels_and_annotations/fancyarrow_demo`.
3177
3178 An instance of any arrow style class is a callable object,
3179 whose call signature is::
3180
3181 __call__(self, path, mutation_size, linewidth, aspect_ratio=1.)
3182
3183 and it returns a tuple of a `.Path` instance and a boolean
3184 value. *path* is a `.Path` instance along which the arrow
3185 will be drawn. *mutation_size* and *aspect_ratio* have the same
3186 meaning as in `BoxStyle`. *linewidth* is a line width to be
3187 stroked. This is meant to be used to correct the location of the
3188 head so that it does not overshoot the destination point, but not all
3189 classes support it.
3190
3191 Notes
3192 -----
3193 *angleA* and *angleB* specify the orientation of the bracket, as either a
3194 clockwise or counterclockwise angle depending on the arrow type. 0 degrees
3195 means perpendicular to the line connecting the arrow's head and tail.
3196
3197 .. plot:: gallery/text_labels_and_annotations/angles_on_bracket_arrows.py
3198 """
3199
3200 _style_list = {}
3201
3202 class _Base:
3203 """
3204 Arrow Transmuter Base class
3205
3206 ArrowTransmuterBase and its derivatives are used to make a fancy
3207 arrow around a given path. The __call__ method returns a path
3208 (which will be used to create a PathPatch instance) and a boolean
3209 value indicating the path is open therefore is not fillable. This
3210 class is not an artist and actual drawing of the fancy arrow is
3211 done by the FancyArrowPatch class.
3212 """
3213
3214 # The derived classes are required to be able to be initialized
3215 # w/o arguments, i.e., all its argument (except self) must have
3216 # the default values.
3217
3218 @staticmethod
3219 def ensure_quadratic_bezier(path):
3220 """
3221 Some ArrowStyle classes only works with a simple quadratic
3222 Bézier curve (created with `.ConnectionStyle.Arc3` or
3223 `.ConnectionStyle.Angle3`). This static method checks if the
3224 provided path is a simple quadratic Bézier curve and returns its
3225 control points if true.
3226 """
3227 segments = list(path.iter_segments())
3228 if (len(segments) != 2 or segments[0][1] != Path.MOVETO or
3229 segments[1][1] != Path.CURVE3):
3230 raise ValueError(
3231 "'path' is not a valid quadratic Bezier curve")
3232 return [*segments[0][0], *segments[1][0]]
3233
3234 def transmute(self, path, mutation_size, linewidth):
3235 """
3236 The transmute method is the very core of the ArrowStyle class and
3237 must be overridden in the subclasses. It receives the *path*
3238 object along which the arrow will be drawn, and the
3239 *mutation_size*, with which the arrow head etc. will be scaled.
3240 The *linewidth* may be used to adjust the path so that it does not
3241 pass beyond the given points. It returns a tuple of a `.Path`
3242 instance and a boolean. The boolean value indicate whether the
3243 path can be filled or not. The return value can also be a list of
3244 paths and list of booleans of the same length.
3245 """
3246 raise NotImplementedError('Derived must override')
3247
3248 def __call__(self, path, mutation_size, linewidth,
3249 aspect_ratio=1.):
3250 """
3251 The __call__ method is a thin wrapper around the transmute method
3252 and takes care of the aspect ratio.
3253 """
3254
3255 if aspect_ratio is not None:
3256 # Squeeze the given height by the aspect_ratio
3257 vertices = path.vertices / [1, aspect_ratio]
3258 path_shrunk = Path(vertices, path.codes)
3259 # call transmute method with squeezed height.
3260 path_mutated, fillable = self.transmute(path_shrunk,
3261 mutation_size,
3262 linewidth)
3263 if np.iterable(fillable):
3264 # Restore the height
3265 path_list = [Path(p.vertices * [1, aspect_ratio], p.codes)
3266 for p in path_mutated]
3267 return path_list, fillable
3268 else:
3269 return path_mutated, fillable
3270 else:
3271 return self.transmute(path, mutation_size, linewidth)
3272
3273 class _Curve(_Base):
3274 """
3275 A simple arrow which will work with any path instance. The
3276 returned path is the concatenation of the original path, and at
3277 most two paths representing the arrow head or bracket at the start
3278 point and at the end point. The arrow heads can be either open
3279 or closed.
3280 """
3281
3282 arrow = "-"
3283 fillbegin = fillend = False # Whether arrows are filled.
3284
3285 def __init__(self, head_length=.4, head_width=.2, widthA=1., widthB=1.,
3286 lengthA=0.2, lengthB=0.2, angleA=0, angleB=0, scaleA=None,
3287 scaleB=None):
3288 """
3289 Parameters
3290 ----------
3291 head_length : float, default: 0.4
3292 Length of the arrow head, relative to *mutation_size*.
3293 head_width : float, default: 0.2
3294 Width of the arrow head, relative to *mutation_size*.
3295 widthA, widthB : float, default: 1.0
3296 Width of the bracket.
3297 lengthA, lengthB : float, default: 0.2
3298 Length of the bracket.
3299 angleA, angleB : float, default: 0
3300 Orientation of the bracket, as a counterclockwise angle.
3301 0 degrees means perpendicular to the line.
3302 scaleA, scaleB : float, default: *mutation_size*
3303 The scale of the brackets.
3304 """
3305
3306 self.head_length, self.head_width = head_length, head_width
3307 self.widthA, self.widthB = widthA, widthB
3308 self.lengthA, self.lengthB = lengthA, lengthB
3309 self.angleA, self.angleB = angleA, angleB
3310 self.scaleA, self.scaleB = scaleA, scaleB
3311
3312 self._beginarrow_head = False
3313 self._beginarrow_bracket = False
3314 self._endarrow_head = False
3315 self._endarrow_bracket = False
3316
3317 if "-" not in self.arrow:
3318 raise ValueError("arrow must have the '-' between "
3319 "the two heads")
3320
3321 beginarrow, endarrow = self.arrow.split("-", 1)
3322
3323 if beginarrow == "<":
3324 self._beginarrow_head = True
3325 self._beginarrow_bracket = False
3326 elif beginarrow == "<|":
3327 self._beginarrow_head = True
3328 self._beginarrow_bracket = False
3329 self.fillbegin = True
3330 elif beginarrow in ("]", "|"):
3331 self._beginarrow_head = False
3332 self._beginarrow_bracket = True
3333
3334 if endarrow == ">":
3335 self._endarrow_head = True
3336 self._endarrow_bracket = False
3337 elif endarrow == "|>":
3338 self._endarrow_head = True
3339 self._endarrow_bracket = False
3340 self.fillend = True
3341 elif endarrow in ("[", "|"):
3342 self._endarrow_head = False
3343 self._endarrow_bracket = True
3344
3345 super().__init__()
3346
3347 def _get_arrow_wedge(self, x0, y0, x1, y1,
3348 head_dist, cos_t, sin_t, linewidth):
3349 """
3350 Return the paths for arrow heads. Since arrow lines are
3351 drawn with capstyle=projected, The arrow goes beyond the
3352 desired point. This method also returns the amount of the path
3353 to be shrunken so that it does not overshoot.
3354 """
3355
3356 # arrow from x0, y0 to x1, y1
3357 dx, dy = x0 - x1, y0 - y1
3358
3359 cp_distance = np.hypot(dx, dy)
3360
3361 # pad_projected : amount of pad to account the
3362 # overshooting of the projection of the wedge
3363 pad_projected = (.5 * linewidth / sin_t)
3364
3365 # Account for division by zero
3366 if cp_distance == 0:
3367 cp_distance = 1
3368
3369 # apply pad for projected edge
3370 ddx = pad_projected * dx / cp_distance
3371 ddy = pad_projected * dy / cp_distance
3372
3373 # offset for arrow wedge
3374 dx = dx / cp_distance * head_dist
3375 dy = dy / cp_distance * head_dist
3376
3377 dx1, dy1 = cos_t * dx + sin_t * dy, -sin_t * dx + cos_t * dy
3378 dx2, dy2 = cos_t * dx - sin_t * dy, sin_t * dx + cos_t * dy
3379
3380 vertices_arrow = [(x1 + ddx + dx1, y1 + ddy + dy1),
3381 (x1 + ddx, y1 + ddy),
3382 (x1 + ddx + dx2, y1 + ddy + dy2)]
3383 codes_arrow = [Path.MOVETO,
3384 Path.LINETO,
3385 Path.LINETO]
3386
3387 return vertices_arrow, codes_arrow, ddx, ddy
3388
3389 def _get_bracket(self, x0, y0,
3390 x1, y1, width, length, angle):
3391
3392 cos_t, sin_t = get_cos_sin(x1, y1, x0, y0)
3393
3394 # arrow from x0, y0 to x1, y1
3395 from matplotlib.bezier import get_normal_points
3396 x1, y1, x2, y2 = get_normal_points(x0, y0, cos_t, sin_t, width)
3397
3398 dx, dy = length * cos_t, length * sin_t
3399
3400 vertices_arrow = [(x1 + dx, y1 + dy),
3401 (x1, y1),
3402 (x2, y2),
3403 (x2 + dx, y2 + dy)]
3404 codes_arrow = [Path.MOVETO,
3405 Path.LINETO,
3406 Path.LINETO,
3407 Path.LINETO]
3408
3409 if angle:
3410 trans = transforms.Affine2D().rotate_deg_around(x0, y0, angle)
3411 vertices_arrow = trans.transform(vertices_arrow)
3412
3413 return vertices_arrow, codes_arrow
3414
3415 def transmute(self, path, mutation_size, linewidth):
3416 # docstring inherited
3417 if self._beginarrow_head or self._endarrow_head:
3418 head_length = self.head_length * mutation_size
3419 head_width = self.head_width * mutation_size
3420 head_dist = np.hypot(head_length, head_width)
3421 cos_t, sin_t = head_length / head_dist, head_width / head_dist
3422
3423 scaleA = mutation_size if self.scaleA is None else self.scaleA
3424 scaleB = mutation_size if self.scaleB is None else self.scaleB
3425
3426 # begin arrow
3427 x0, y0 = path.vertices[0]
3428 x1, y1 = path.vertices[1]
3429
3430 # If there is no room for an arrow and a line, then skip the arrow
3431 has_begin_arrow = self._beginarrow_head and (x0, y0) != (x1, y1)
3432 verticesA, codesA, ddxA, ddyA = (
3433 self._get_arrow_wedge(x1, y1, x0, y0,
3434 head_dist, cos_t, sin_t, linewidth)
3435 if has_begin_arrow
3436 else ([], [], 0, 0)
3437 )
3438
3439 # end arrow
3440 x2, y2 = path.vertices[-2]
3441 x3, y3 = path.vertices[-1]
3442
3443 # If there is no room for an arrow and a line, then skip the arrow
3444 has_end_arrow = self._endarrow_head and (x2, y2) != (x3, y3)
3445 verticesB, codesB, ddxB, ddyB = (
3446 self._get_arrow_wedge(x2, y2, x3, y3,
3447 head_dist, cos_t, sin_t, linewidth)
3448 if has_end_arrow
3449 else ([], [], 0, 0)
3450 )
3451
3452 # This simple code will not work if ddx, ddy is greater than the
3453 # separation between vertices.
3454 paths = [Path(np.concatenate([[(x0 + ddxA, y0 + ddyA)],
3455 path.vertices[1:-1],
3456 [(x3 + ddxB, y3 + ddyB)]]),
3457 path.codes)]
3458 fills = [False]
3459
3460 if has_begin_arrow:
3461 if self.fillbegin:
3462 paths.append(
3463 Path([*verticesA, (0, 0)], [*codesA, Path.CLOSEPOLY]))
3464 fills.append(True)
3465 else:
3466 paths.append(Path(verticesA, codesA))
3467 fills.append(False)
3468 elif self._beginarrow_bracket:
3469 x0, y0 = path.vertices[0]
3470 x1, y1 = path.vertices[1]
3471 verticesA, codesA = self._get_bracket(x0, y0, x1, y1,
3472 self.widthA * scaleA,
3473 self.lengthA * scaleA,
3474 self.angleA)
3475
3476 paths.append(Path(verticesA, codesA))
3477 fills.append(False)
3478
3479 if has_end_arrow:
3480 if self.fillend:
3481 fills.append(True)
3482 paths.append(
3483 Path([*verticesB, (0, 0)], [*codesB, Path.CLOSEPOLY]))
3484 else:
3485 fills.append(False)
3486 paths.append(Path(verticesB, codesB))
3487 elif self._endarrow_bracket:
3488 x0, y0 = path.vertices[-1]
3489 x1, y1 = path.vertices[-2]
3490 verticesB, codesB = self._get_bracket(x0, y0, x1, y1,
3491 self.widthB * scaleB,
3492 self.lengthB * scaleB,
3493 self.angleB)
3494
3495 paths.append(Path(verticesB, codesB))
3496 fills.append(False)
3497
3498 return paths, fills
3499
3500 @_register_style(_style_list, name="-")
3501 class Curve(_Curve):
3502 """A simple curve without any arrow head."""
3503
3504 def __init__(self): # hide head_length, head_width
3505 # These attributes (whose values come from backcompat) only matter
3506 # if someone modifies beginarrow/etc. on an ArrowStyle instance.
3507 super().__init__(head_length=.2, head_width=.1)
3508
3509 @_register_style(_style_list, name="<-")
3510 class CurveA(_Curve):
3511 """An arrow with a head at its start point."""
3512 arrow = "<-"
3513
3514 @_register_style(_style_list, name="->")
3515 class CurveB(_Curve):
3516 """An arrow with a head at its end point."""
3517 arrow = "->"
3518
3519 @_register_style(_style_list, name="<->")
3520 class CurveAB(_Curve):
3521 """An arrow with heads both at the start and the end point."""
3522 arrow = "<->"
3523
3524 @_register_style(_style_list, name="<|-")
3525 class CurveFilledA(_Curve):
3526 """An arrow with filled triangle head at the start."""
3527 arrow = "<|-"
3528
3529 @_register_style(_style_list, name="-|>")
3530 class CurveFilledB(_Curve):
3531 """An arrow with filled triangle head at the end."""
3532 arrow = "-|>"
3533
3534 @_register_style(_style_list, name="<|-|>")
3535 class CurveFilledAB(_Curve):
3536 """An arrow with filled triangle heads at both ends."""
3537 arrow = "<|-|>"
3538
3539 @_register_style(_style_list, name="]-")
3540 class BracketA(_Curve):
3541 """An arrow with an outward square bracket at its start."""
3542 arrow = "]-"
3543
3544 def __init__(self, widthA=1., lengthA=0.2, angleA=0):
3545 """
3546 Parameters
3547 ----------
3548 widthA : float, default: 1.0
3549 Width of the bracket.
3550 lengthA : float, default: 0.2
3551 Length of the bracket.
3552 angleA : float, default: 0 degrees
3553 Orientation of the bracket, as a counterclockwise angle.
3554 0 degrees means perpendicular to the line.
3555 """
3556 super().__init__(widthA=widthA, lengthA=lengthA, angleA=angleA)
3557
3558 @_register_style(_style_list, name="-[")
3559 class BracketB(_Curve):
3560 """An arrow with an outward square bracket at its end."""
3561 arrow = "-["
3562
3563 def __init__(self, widthB=1., lengthB=0.2, angleB=0):
3564 """
3565 Parameters
3566 ----------
3567 widthB : float, default: 1.0
3568 Width of the bracket.
3569 lengthB : float, default: 0.2
3570 Length of the bracket.
3571 angleB : float, default: 0 degrees
3572 Orientation of the bracket, as a counterclockwise angle.
3573 0 degrees means perpendicular to the line.
3574 """
3575 super().__init__(widthB=widthB, lengthB=lengthB, angleB=angleB)
3576
3577 @_register_style(_style_list, name="]-[")
3578 class BracketAB(_Curve):
3579 """An arrow with outward square brackets at both ends."""
3580 arrow = "]-["
3581
3582 def __init__(self,
3583 widthA=1., lengthA=0.2, angleA=0,
3584 widthB=1., lengthB=0.2, angleB=0):
3585 """
3586 Parameters
3587 ----------
3588 widthA, widthB : float, default: 1.0
3589 Width of the bracket.
3590 lengthA, lengthB : float, default: 0.2
3591 Length of the bracket.
3592 angleA, angleB : float, default: 0 degrees
3593 Orientation of the bracket, as a counterclockwise angle.
3594 0 degrees means perpendicular to the line.
3595 """
3596 super().__init__(widthA=widthA, lengthA=lengthA, angleA=angleA,
3597 widthB=widthB, lengthB=lengthB, angleB=angleB)
3598
3599 @_register_style(_style_list, name="|-|")
3600 class BarAB(_Curve):
3601 """An arrow with vertical bars ``|`` at both ends."""
3602 arrow = "|-|"
3603
3604 def __init__(self, widthA=1., angleA=0, widthB=1., angleB=0):
3605 """
3606 Parameters
3607 ----------
3608 widthA, widthB : float, default: 1.0
3609 Width of the bracket.
3610 angleA, angleB : float, default: 0 degrees
3611 Orientation of the bracket, as a counterclockwise angle.
3612 0 degrees means perpendicular to the line.
3613 """
3614 super().__init__(widthA=widthA, lengthA=0, angleA=angleA,
3615 widthB=widthB, lengthB=0, angleB=angleB)
3616
3617 @_register_style(_style_list, name=']->')
3618 class BracketCurve(_Curve):
3619 """
3620 An arrow with an outward square bracket at its start and a head at
3621 the end.
3622 """
3623 arrow = "]->"
3624
3625 def __init__(self, widthA=1., lengthA=0.2, angleA=None):
3626 """
3627 Parameters
3628 ----------
3629 widthA : float, default: 1.0
3630 Width of the bracket.
3631 lengthA : float, default: 0.2
3632 Length of the bracket.
3633 angleA : float, default: 0 degrees
3634 Orientation of the bracket, as a counterclockwise angle.
3635 0 degrees means perpendicular to the line.
3636 """
3637 super().__init__(widthA=widthA, lengthA=lengthA, angleA=angleA)
3638
3639 @_register_style(_style_list, name='<-[')
3640 class CurveBracket(_Curve):
3641 """
3642 An arrow with an outward square bracket at its end and a head at
3643 the start.
3644 """
3645 arrow = "<-["
3646
3647 def __init__(self, widthB=1., lengthB=0.2, angleB=None):
3648 """
3649 Parameters
3650 ----------
3651 widthB : float, default: 1.0
3652 Width of the bracket.
3653 lengthB : float, default: 0.2
3654 Length of the bracket.
3655 angleB : float, default: 0 degrees
3656 Orientation of the bracket, as a counterclockwise angle.
3657 0 degrees means perpendicular to the line.
3658 """
3659 super().__init__(widthB=widthB, lengthB=lengthB, angleB=angleB)
3660
3661 @_register_style(_style_list)
3662 class Simple(_Base):
3663 """A simple arrow. Only works with a quadratic Bézier curve."""
3664
3665 def __init__(self, head_length=.5, head_width=.5, tail_width=.2):
3666 """
3667 Parameters
3668 ----------
3669 head_length : float, default: 0.5
3670 Length of the arrow head.
3671
3672 head_width : float, default: 0.5
3673 Width of the arrow head.
3674
3675 tail_width : float, default: 0.2
3676 Width of the arrow tail.
3677 """
3678 self.head_length, self.head_width, self.tail_width = \
3679 head_length, head_width, tail_width
3680 super().__init__()
3681
3682 def transmute(self, path, mutation_size, linewidth):
3683 # docstring inherited
3684 x0, y0, x1, y1, x2, y2 = self.ensure_quadratic_bezier(path)
3685
3686 # divide the path into a head and a tail
3687 head_length = self.head_length * mutation_size
3688 in_f = inside_circle(x2, y2, head_length)
3689 arrow_path = [(x0, y0), (x1, y1), (x2, y2)]
3690
3691 try:
3692 arrow_out, arrow_in = \
3693 split_bezier_intersecting_with_closedpath(arrow_path, in_f)
3694 except NonIntersectingPathException:
3695 # if this happens, make a straight line of the head_length
3696 # long.
3697 x0, y0 = _point_along_a_line(x2, y2, x1, y1, head_length)
3698 x1n, y1n = 0.5 * (x0 + x2), 0.5 * (y0 + y2)
3699 arrow_in = [(x0, y0), (x1n, y1n), (x2, y2)]
3700 arrow_out = None
3701
3702 # head
3703 head_width = self.head_width * mutation_size
3704 head_left, head_right = make_wedged_bezier2(arrow_in,
3705 head_width / 2., wm=.5)
3706
3707 # tail
3708 if arrow_out is not None:
3709 tail_width = self.tail_width * mutation_size
3710 tail_left, tail_right = get_parallels(arrow_out,
3711 tail_width / 2.)
3712
3713 patch_path = [(Path.MOVETO, tail_right[0]),
3714 (Path.CURVE3, tail_right[1]),
3715 (Path.CURVE3, tail_right[2]),
3716 (Path.LINETO, head_right[0]),
3717 (Path.CURVE3, head_right[1]),
3718 (Path.CURVE3, head_right[2]),
3719 (Path.CURVE3, head_left[1]),
3720 (Path.CURVE3, head_left[0]),
3721 (Path.LINETO, tail_left[2]),
3722 (Path.CURVE3, tail_left[1]),
3723 (Path.CURVE3, tail_left[0]),
3724 (Path.LINETO, tail_right[0]),
3725 (Path.CLOSEPOLY, tail_right[0]),
3726 ]
3727 else:
3728 patch_path = [(Path.MOVETO, head_right[0]),
3729 (Path.CURVE3, head_right[1]),
3730 (Path.CURVE3, head_right[2]),
3731 (Path.CURVE3, head_left[1]),
3732 (Path.CURVE3, head_left[0]),
3733 (Path.CLOSEPOLY, head_left[0]),
3734 ]
3735
3736 path = Path([p for c, p in patch_path], [c for c, p in patch_path])
3737
3738 return path, True
3739
3740 @_register_style(_style_list)
3741 class Fancy(_Base):
3742 """A fancy arrow. Only works with a quadratic Bézier curve."""
3743
3744 def __init__(self, head_length=.4, head_width=.4, tail_width=.4):
3745 """
3746 Parameters
3747 ----------
3748 head_length : float, default: 0.4
3749 Length of the arrow head.
3750
3751 head_width : float, default: 0.4
3752 Width of the arrow head.
3753
3754 tail_width : float, default: 0.4
3755 Width of the arrow tail.
3756 """
3757 self.head_length, self.head_width, self.tail_width = \
3758 head_length, head_width, tail_width
3759 super().__init__()
3760
3761 def transmute(self, path, mutation_size, linewidth):
3762 # docstring inherited
3763 x0, y0, x1, y1, x2, y2 = self.ensure_quadratic_bezier(path)
3764
3765 # divide the path into a head and a tail
3766 head_length = self.head_length * mutation_size
3767 arrow_path = [(x0, y0), (x1, y1), (x2, y2)]
3768
3769 # path for head
3770 in_f = inside_circle(x2, y2, head_length)
3771 try:
3772 path_out, path_in = split_bezier_intersecting_with_closedpath(
3773 arrow_path, in_f)
3774 except NonIntersectingPathException:
3775 # if this happens, make a straight line of the head_length
3776 # long.
3777 x0, y0 = _point_along_a_line(x2, y2, x1, y1, head_length)
3778 x1n, y1n = 0.5 * (x0 + x2), 0.5 * (y0 + y2)
3779 arrow_path = [(x0, y0), (x1n, y1n), (x2, y2)]
3780 path_head = arrow_path
3781 else:
3782 path_head = path_in
3783
3784 # path for head
3785 in_f = inside_circle(x2, y2, head_length * .8)
3786 path_out, path_in = split_bezier_intersecting_with_closedpath(
3787 arrow_path, in_f)
3788 path_tail = path_out
3789
3790 # head
3791 head_width = self.head_width * mutation_size
3792 head_l, head_r = make_wedged_bezier2(path_head,
3793 head_width / 2.,
3794 wm=.6)
3795
3796 # tail
3797 tail_width = self.tail_width * mutation_size
3798 tail_left, tail_right = make_wedged_bezier2(path_tail,
3799 tail_width * .5,
3800 w1=1., wm=0.6, w2=0.3)
3801
3802 # path for head
3803 in_f = inside_circle(x0, y0, tail_width * .3)
3804 path_in, path_out = split_bezier_intersecting_with_closedpath(
3805 arrow_path, in_f)
3806 tail_start = path_in[-1]
3807
3808 head_right, head_left = head_r, head_l
3809 patch_path = [(Path.MOVETO, tail_start),
3810 (Path.LINETO, tail_right[0]),
3811 (Path.CURVE3, tail_right[1]),
3812 (Path.CURVE3, tail_right[2]),
3813 (Path.LINETO, head_right[0]),
3814 (Path.CURVE3, head_right[1]),
3815 (Path.CURVE3, head_right[2]),
3816 (Path.CURVE3, head_left[1]),
3817 (Path.CURVE3, head_left[0]),
3818 (Path.LINETO, tail_left[2]),
3819 (Path.CURVE3, tail_left[1]),
3820 (Path.CURVE3, tail_left[0]),
3821 (Path.LINETO, tail_start),
3822 (Path.CLOSEPOLY, tail_start),
3823 ]
3824 path = Path([p for c, p in patch_path], [c for c, p in patch_path])
3825
3826 return path, True
3827
3828 @_register_style(_style_list)
3829 class Wedge(_Base):
3830 """
3831 Wedge(?) shape. Only works with a quadratic Bézier curve. The
3832 start point has a width of the *tail_width* and the end point has a
3833 width of 0. At the middle, the width is *shrink_factor*x*tail_width*.
3834 """
3835
3836 def __init__(self, tail_width=.3, shrink_factor=0.5):
3837 """
3838 Parameters
3839 ----------
3840 tail_width : float, default: 0.3
3841 Width of the tail.
3842
3843 shrink_factor : float, default: 0.5
3844 Fraction of the arrow width at the middle point.
3845 """
3846 self.tail_width = tail_width
3847 self.shrink_factor = shrink_factor
3848 super().__init__()
3849
3850 def transmute(self, path, mutation_size, linewidth):
3851 # docstring inherited
3852 x0, y0, x1, y1, x2, y2 = self.ensure_quadratic_bezier(path)
3853
3854 arrow_path = [(x0, y0), (x1, y1), (x2, y2)]
3855 b_plus, b_minus = make_wedged_bezier2(
3856 arrow_path,
3857 self.tail_width * mutation_size / 2.,
3858 wm=self.shrink_factor)
3859
3860 patch_path = [(Path.MOVETO, b_plus[0]),
3861 (Path.CURVE3, b_plus[1]),
3862 (Path.CURVE3, b_plus[2]),
3863 (Path.LINETO, b_minus[2]),
3864 (Path.CURVE3, b_minus[1]),
3865 (Path.CURVE3, b_minus[0]),
3866 (Path.CLOSEPOLY, b_minus[0]),
3867 ]
3868 path = Path([p for c, p in patch_path], [c for c, p in patch_path])
3869
3870 return path, True
3871
3872
3873class FancyBboxPatch(Patch):
3874 """
3875 A fancy box around a rectangle with lower left at *xy* = (*x*, *y*)
3876 with specified width and height.
3877
3878 `.FancyBboxPatch` is similar to `.Rectangle`, but it draws a fancy box
3879 around the rectangle. The transformation of the rectangle box to the
3880 fancy box is delegated to the style classes defined in `.BoxStyle`.
3881 """
3882
3883 _edge_default = True
3884
3885 def __str__(self):
3886 s = self.__class__.__name__ + "((%g, %g), width=%g, height=%g)"
3887 return s % (self._x, self._y, self._width, self._height)
3888
3889 @_docstring.dedent_interpd
3890 def __init__(self, xy, width, height, boxstyle="round", *,
3891 mutation_scale=1, mutation_aspect=1, **kwargs):
3892 """
3893 Parameters
3894 ----------
3895 xy : (float, float)
3896 The lower left corner of the box.
3897
3898 width : float
3899 The width of the box.
3900
3901 height : float
3902 The height of the box.
3903
3904 boxstyle : str or `~matplotlib.patches.BoxStyle`
3905 The style of the fancy box. This can either be a `.BoxStyle`
3906 instance or a string of the style name and optionally comma
3907 separated attributes (e.g. "Round, pad=0.2"). This string is
3908 passed to `.BoxStyle` to construct a `.BoxStyle` object. See
3909 there for a full documentation.
3910
3911 The following box styles are available:
3912
3913 %(BoxStyle:table)s
3914
3915 mutation_scale : float, default: 1
3916 Scaling factor applied to the attributes of the box style
3917 (e.g. pad or rounding_size).
3918
3919 mutation_aspect : float, default: 1
3920 The height of the rectangle will be squeezed by this value before
3921 the mutation and the mutated box will be stretched by the inverse
3922 of it. For example, this allows different horizontal and vertical
3923 padding.
3924
3925 Other Parameters
3926 ----------------
3927 **kwargs : `~matplotlib.patches.Patch` properties
3928
3929 %(Patch:kwdoc)s
3930 """
3931
3932 super().__init__(**kwargs)
3933 self._x, self._y = xy
3934 self._width = width
3935 self._height = height
3936 self.set_boxstyle(boxstyle)
3937 self._mutation_scale = mutation_scale
3938 self._mutation_aspect = mutation_aspect
3939 self.stale = True
3940
3941 @_docstring.dedent_interpd
3942 def set_boxstyle(self, boxstyle=None, **kwargs):
3943 """
3944 Set the box style, possibly with further attributes.
3945
3946 Attributes from the previous box style are not reused.
3947
3948 Without argument (or with ``boxstyle=None``), the available box styles
3949 are returned as a human-readable string.
3950
3951 Parameters
3952 ----------
3953 boxstyle : str or `~matplotlib.patches.BoxStyle`
3954 The style of the box: either a `.BoxStyle` instance, or a string,
3955 which is the style name and optionally comma separated attributes
3956 (e.g. "Round,pad=0.2"). Such a string is used to construct a
3957 `.BoxStyle` object, as documented in that class.
3958
3959 The following box styles are available:
3960
3961 %(BoxStyle:table_and_accepts)s
3962
3963 **kwargs
3964 Additional attributes for the box style. See the table above for
3965 supported parameters.
3966
3967 Examples
3968 --------
3969 ::
3970
3971 set_boxstyle("Round,pad=0.2")
3972 set_boxstyle("round", pad=0.2)
3973 """
3974 if boxstyle is None:
3975 return BoxStyle.pprint_styles()
3976 self._bbox_transmuter = (
3977 BoxStyle(boxstyle, **kwargs)
3978 if isinstance(boxstyle, str) else boxstyle)
3979 self.stale = True
3980
3981 def get_boxstyle(self):
3982 """Return the boxstyle object."""
3983 return self._bbox_transmuter
3984
3985 def set_mutation_scale(self, scale):
3986 """
3987 Set the mutation scale.
3988
3989 Parameters
3990 ----------
3991 scale : float
3992 """
3993 self._mutation_scale = scale
3994 self.stale = True
3995
3996 def get_mutation_scale(self):
3997 """Return the mutation scale."""
3998 return self._mutation_scale
3999
4000 def set_mutation_aspect(self, aspect):
4001 """
4002 Set the aspect ratio of the bbox mutation.
4003
4004 Parameters
4005 ----------
4006 aspect : float
4007 """
4008 self._mutation_aspect = aspect
4009 self.stale = True
4010
4011 def get_mutation_aspect(self):
4012 """Return the aspect ratio of the bbox mutation."""
4013 return (self._mutation_aspect if self._mutation_aspect is not None
4014 else 1) # backcompat.
4015
4016 def get_path(self):
4017 """Return the mutated path of the rectangle."""
4018 boxstyle = self.get_boxstyle()
4019 m_aspect = self.get_mutation_aspect()
4020 # Call boxstyle with y, height squeezed by aspect_ratio.
4021 path = boxstyle(self._x, self._y / m_aspect,
4022 self._width, self._height / m_aspect,
4023 self.get_mutation_scale())
4024 return Path(path.vertices * [1, m_aspect], path.codes) # Unsqueeze y.
4025
4026 # Following methods are borrowed from the Rectangle class.
4027
4028 def get_x(self):
4029 """Return the left coord of the rectangle."""
4030 return self._x
4031
4032 def get_y(self):
4033 """Return the bottom coord of the rectangle."""
4034 return self._y
4035
4036 def get_width(self):
4037 """Return the width of the rectangle."""
4038 return self._width
4039
4040 def get_height(self):
4041 """Return the height of the rectangle."""
4042 return self._height
4043
4044 def set_x(self, x):
4045 """
4046 Set the left coord of the rectangle.
4047
4048 Parameters
4049 ----------
4050 x : float
4051 """
4052 self._x = x
4053 self.stale = True
4054
4055 def set_y(self, y):
4056 """
4057 Set the bottom coord of the rectangle.
4058
4059 Parameters
4060 ----------
4061 y : float
4062 """
4063 self._y = y
4064 self.stale = True
4065
4066 def set_width(self, w):
4067 """
4068 Set the rectangle width.
4069
4070 Parameters
4071 ----------
4072 w : float
4073 """
4074 self._width = w
4075 self.stale = True
4076
4077 def set_height(self, h):
4078 """
4079 Set the rectangle height.
4080
4081 Parameters
4082 ----------
4083 h : float
4084 """
4085 self._height = h
4086 self.stale = True
4087
4088 def set_bounds(self, *args):
4089 """
4090 Set the bounds of the rectangle.
4091
4092 Call signatures::
4093
4094 set_bounds(left, bottom, width, height)
4095 set_bounds((left, bottom, width, height))
4096
4097 Parameters
4098 ----------
4099 left, bottom : float
4100 The coordinates of the bottom left corner of the rectangle.
4101 width, height : float
4102 The width/height of the rectangle.
4103 """
4104 if len(args) == 1:
4105 l, b, w, h = args[0]
4106 else:
4107 l, b, w, h = args
4108 self._x = l
4109 self._y = b
4110 self._width = w
4111 self._height = h
4112 self.stale = True
4113
4114 def get_bbox(self):
4115 """Return the `.Bbox`."""
4116 return transforms.Bbox.from_bounds(self._x, self._y,
4117 self._width, self._height)
4118
4119
4120class FancyArrowPatch(Patch):
4121 """
4122 A fancy arrow patch.
4123
4124 It draws an arrow using the `ArrowStyle`. It is primarily used by the
4125 `~.axes.Axes.annotate` method. For most purposes, use the annotate method for
4126 drawing arrows.
4127
4128 The head and tail positions are fixed at the specified start and end points
4129 of the arrow, but the size and shape (in display coordinates) of the arrow
4130 does not change when the axis is moved or zoomed.
4131 """
4132 _edge_default = True
4133
4134 def __str__(self):
4135 if self._posA_posB is not None:
4136 (x1, y1), (x2, y2) = self._posA_posB
4137 return f"{type(self).__name__}(({x1:g}, {y1:g})->({x2:g}, {y2:g}))"
4138 else:
4139 return f"{type(self).__name__}({self._path_original})"
4140
4141 @_docstring.dedent_interpd
4142 def __init__(self, posA=None, posB=None, *,
4143 path=None, arrowstyle="simple", connectionstyle="arc3",
4144 patchA=None, patchB=None, shrinkA=2, shrinkB=2,
4145 mutation_scale=1, mutation_aspect=1, **kwargs):
4146 """
4147 There are two ways for defining an arrow:
4148
4149 - If *posA* and *posB* are given, a path connecting two points is
4150 created according to *connectionstyle*. The path will be
4151 clipped with *patchA* and *patchB* and further shrunken by
4152 *shrinkA* and *shrinkB*. An arrow is drawn along this
4153 resulting path using the *arrowstyle* parameter.
4154
4155 - Alternatively if *path* is provided, an arrow is drawn along this
4156 path and *patchA*, *patchB*, *shrinkA*, and *shrinkB* are ignored.
4157
4158 Parameters
4159 ----------
4160 posA, posB : (float, float), default: None
4161 (x, y) coordinates of arrow tail and arrow head respectively.
4162
4163 path : `~matplotlib.path.Path`, default: None
4164 If provided, an arrow is drawn along this path and *patchA*,
4165 *patchB*, *shrinkA*, and *shrinkB* are ignored.
4166
4167 arrowstyle : str or `.ArrowStyle`, default: 'simple'
4168 The `.ArrowStyle` with which the fancy arrow is drawn. If a
4169 string, it should be one of the available arrowstyle names, with
4170 optional comma-separated attributes. The optional attributes are
4171 meant to be scaled with the *mutation_scale*. The following arrow
4172 styles are available:
4173
4174 %(ArrowStyle:table)s
4175
4176 connectionstyle : str or `.ConnectionStyle` or None, optional, \
4177default: 'arc3'
4178 The `.ConnectionStyle` with which *posA* and *posB* are connected.
4179 If a string, it should be one of the available connectionstyle
4180 names, with optional comma-separated attributes. The following
4181 connection styles are available:
4182
4183 %(ConnectionStyle:table)s
4184
4185 patchA, patchB : `~matplotlib.patches.Patch`, default: None
4186 Head and tail patches, respectively.
4187
4188 shrinkA, shrinkB : float, default: 2
4189 Shrink amount, in points, of the tail and head of the arrow respectively.
4190
4191 mutation_scale : float, default: 1
4192 Value with which attributes of *arrowstyle* (e.g., *head_length*)
4193 will be scaled.
4194
4195 mutation_aspect : None or float, default: None
4196 The height of the rectangle will be squeezed by this value before
4197 the mutation and the mutated box will be stretched by the inverse
4198 of it.
4199
4200 Other Parameters
4201 ----------------
4202 **kwargs : `~matplotlib.patches.Patch` properties, optional
4203 Here is a list of available `.Patch` properties:
4204
4205 %(Patch:kwdoc)s
4206
4207 In contrast to other patches, the default ``capstyle`` and
4208 ``joinstyle`` for `FancyArrowPatch` are set to ``"round"``.
4209 """
4210 # Traditionally, the cap- and joinstyle for FancyArrowPatch are round
4211 kwargs.setdefault("joinstyle", JoinStyle.round)
4212 kwargs.setdefault("capstyle", CapStyle.round)
4213
4214 super().__init__(**kwargs)
4215
4216 if posA is not None and posB is not None and path is None:
4217 self._posA_posB = [posA, posB]
4218
4219 if connectionstyle is None:
4220 connectionstyle = "arc3"
4221 self.set_connectionstyle(connectionstyle)
4222
4223 elif posA is None and posB is None and path is not None:
4224 self._posA_posB = None
4225 else:
4226 raise ValueError("Either posA and posB, or path need to provided")
4227
4228 self.patchA = patchA
4229 self.patchB = patchB
4230 self.shrinkA = shrinkA
4231 self.shrinkB = shrinkB
4232
4233 self._path_original = path
4234
4235 self.set_arrowstyle(arrowstyle)
4236
4237 self._mutation_scale = mutation_scale
4238 self._mutation_aspect = mutation_aspect
4239
4240 self._dpi_cor = 1.0
4241
4242 def set_positions(self, posA, posB):
4243 """
4244 Set the start and end positions of the connecting path.
4245
4246 Parameters
4247 ----------
4248 posA, posB : None, tuple
4249 (x, y) coordinates of arrow tail and arrow head respectively. If
4250 `None` use current value.
4251 """
4252 if posA is not None:
4253 self._posA_posB[0] = posA
4254 if posB is not None:
4255 self._posA_posB[1] = posB
4256 self.stale = True
4257
4258 def set_patchA(self, patchA):
4259 """
4260 Set the tail patch.
4261
4262 Parameters
4263 ----------
4264 patchA : `.patches.Patch`
4265 """
4266 self.patchA = patchA
4267 self.stale = True
4268
4269 def set_patchB(self, patchB):
4270 """
4271 Set the head patch.
4272
4273 Parameters
4274 ----------
4275 patchB : `.patches.Patch`
4276 """
4277 self.patchB = patchB
4278 self.stale = True
4279
4280 @_docstring.dedent_interpd
4281 def set_connectionstyle(self, connectionstyle=None, **kwargs):
4282 """
4283 Set the connection style, possibly with further attributes.
4284
4285 Attributes from the previous connection style are not reused.
4286
4287 Without argument (or with ``connectionstyle=None``), the available box
4288 styles are returned as a human-readable string.
4289
4290 Parameters
4291 ----------
4292 connectionstyle : str or `~matplotlib.patches.ConnectionStyle`
4293 The style of the connection: either a `.ConnectionStyle` instance,
4294 or a string, which is the style name and optionally comma separated
4295 attributes (e.g. "Arc,armA=30,rad=10"). Such a string is used to
4296 construct a `.ConnectionStyle` object, as documented in that class.
4297
4298 The following connection styles are available:
4299
4300 %(ConnectionStyle:table_and_accepts)s
4301
4302 **kwargs
4303 Additional attributes for the connection style. See the table above
4304 for supported parameters.
4305
4306 Examples
4307 --------
4308 ::
4309
4310 set_connectionstyle("Arc,armA=30,rad=10")
4311 set_connectionstyle("arc", armA=30, rad=10)
4312 """
4313 if connectionstyle is None:
4314 return ConnectionStyle.pprint_styles()
4315 self._connector = (
4316 ConnectionStyle(connectionstyle, **kwargs)
4317 if isinstance(connectionstyle, str) else connectionstyle)
4318 self.stale = True
4319
4320 def get_connectionstyle(self):
4321 """Return the `ConnectionStyle` used."""
4322 return self._connector
4323
4324 def set_arrowstyle(self, arrowstyle=None, **kwargs):
4325 """
4326 Set the arrow style, possibly with further attributes.
4327
4328 Attributes from the previous arrow style are not reused.
4329
4330 Without argument (or with ``arrowstyle=None``), the available box
4331 styles are returned as a human-readable string.
4332
4333 Parameters
4334 ----------
4335 arrowstyle : str or `~matplotlib.patches.ArrowStyle`
4336 The style of the arrow: either a `.ArrowStyle` instance, or a
4337 string, which is the style name and optionally comma separated
4338 attributes (e.g. "Fancy,head_length=0.2"). Such a string is used to
4339 construct a `.ArrowStyle` object, as documented in that class.
4340
4341 The following arrow styles are available:
4342
4343 %(ArrowStyle:table_and_accepts)s
4344
4345 **kwargs
4346 Additional attributes for the arrow style. See the table above for
4347 supported parameters.
4348
4349 Examples
4350 --------
4351 ::
4352
4353 set_arrowstyle("Fancy,head_length=0.2")
4354 set_arrowstyle("fancy", head_length=0.2)
4355 """
4356 if arrowstyle is None:
4357 return ArrowStyle.pprint_styles()
4358 self._arrow_transmuter = (
4359 ArrowStyle(arrowstyle, **kwargs)
4360 if isinstance(arrowstyle, str) else arrowstyle)
4361 self.stale = True
4362
4363 def get_arrowstyle(self):
4364 """Return the arrowstyle object."""
4365 return self._arrow_transmuter
4366
4367 def set_mutation_scale(self, scale):
4368 """
4369 Set the mutation scale.
4370
4371 Parameters
4372 ----------
4373 scale : float
4374 """
4375 self._mutation_scale = scale
4376 self.stale = True
4377
4378 def get_mutation_scale(self):
4379 """
4380 Return the mutation scale.
4381
4382 Returns
4383 -------
4384 scalar
4385 """
4386 return self._mutation_scale
4387
4388 def set_mutation_aspect(self, aspect):
4389 """
4390 Set the aspect ratio of the bbox mutation.
4391
4392 Parameters
4393 ----------
4394 aspect : float
4395 """
4396 self._mutation_aspect = aspect
4397 self.stale = True
4398
4399 def get_mutation_aspect(self):
4400 """Return the aspect ratio of the bbox mutation."""
4401 return (self._mutation_aspect if self._mutation_aspect is not None
4402 else 1) # backcompat.
4403
4404 def get_path(self):
4405 """Return the path of the arrow in the data coordinates."""
4406 # The path is generated in display coordinates, then converted back to
4407 # data coordinates.
4408 _path, fillable = self._get_path_in_displaycoord()
4409 if np.iterable(fillable):
4410 _path = Path.make_compound_path(*_path)
4411 return self.get_transform().inverted().transform_path(_path)
4412
4413 def _get_path_in_displaycoord(self):
4414 """Return the mutated path of the arrow in display coordinates."""
4415 dpi_cor = self._dpi_cor
4416
4417 if self._posA_posB is not None:
4418 posA = self._convert_xy_units(self._posA_posB[0])
4419 posB = self._convert_xy_units(self._posA_posB[1])
4420 (posA, posB) = self.get_transform().transform((posA, posB))
4421 _path = self.get_connectionstyle()(posA, posB,
4422 patchA=self.patchA,
4423 patchB=self.patchB,
4424 shrinkA=self.shrinkA * dpi_cor,
4425 shrinkB=self.shrinkB * dpi_cor
4426 )
4427 else:
4428 _path = self.get_transform().transform_path(self._path_original)
4429
4430 _path, fillable = self.get_arrowstyle()(
4431 _path,
4432 self.get_mutation_scale() * dpi_cor,
4433 self.get_linewidth() * dpi_cor,
4434 self.get_mutation_aspect())
4435
4436 return _path, fillable
4437
4438 def draw(self, renderer):
4439 if not self.get_visible():
4440 return
4441
4442 # FIXME: dpi_cor is for the dpi-dependency of the linewidth. There
4443 # could be room for improvement. Maybe _get_path_in_displaycoord could
4444 # take a renderer argument, but get_path should be adapted too.
4445 self._dpi_cor = renderer.points_to_pixels(1.)
4446 path, fillable = self._get_path_in_displaycoord()
4447
4448 if not np.iterable(fillable):
4449 path = [path]
4450 fillable = [fillable]
4451
4452 affine = transforms.IdentityTransform()
4453
4454 self._draw_paths_with_artist_properties(
4455 renderer,
4456 [(p, affine, self._facecolor if f and self._facecolor[3] else None)
4457 for p, f in zip(path, fillable)])
4458
4459
4460class ConnectionPatch(FancyArrowPatch):
4461 """A patch that connects two points (possibly in different Axes)."""
4462
4463 def __str__(self):
4464 return "ConnectionPatch((%g, %g), (%g, %g))" % \
4465 (self.xy1[0], self.xy1[1], self.xy2[0], self.xy2[1])
4466
4467 @_docstring.dedent_interpd
4468 def __init__(self, xyA, xyB, coordsA, coordsB=None, *,
4469 axesA=None, axesB=None,
4470 arrowstyle="-",
4471 connectionstyle="arc3",
4472 patchA=None,
4473 patchB=None,
4474 shrinkA=0.,
4475 shrinkB=0.,
4476 mutation_scale=10.,
4477 mutation_aspect=None,
4478 clip_on=False,
4479 **kwargs):
4480 """
4481 Connect point *xyA* in *coordsA* with point *xyB* in *coordsB*.
4482
4483 Valid keys are
4484
4485 =============== ======================================================
4486 Key Description
4487 =============== ======================================================
4488 arrowstyle the arrow style
4489 connectionstyle the connection style
4490 relpos default is (0.5, 0.5)
4491 patchA default is bounding box of the text
4492 patchB default is None
4493 shrinkA default is 2 points
4494 shrinkB default is 2 points
4495 mutation_scale default is text size (in points)
4496 mutation_aspect default is 1.
4497 ? any key for `matplotlib.patches.PathPatch`
4498 =============== ======================================================
4499
4500 *coordsA* and *coordsB* are strings that indicate the
4501 coordinates of *xyA* and *xyB*.
4502
4503 ==================== ==================================================
4504 Property Description
4505 ==================== ==================================================
4506 'figure points' points from the lower left corner of the figure
4507 'figure pixels' pixels from the lower left corner of the figure
4508 'figure fraction' 0, 0 is lower left of figure and 1, 1 is upper
4509 right
4510 'subfigure points' points from the lower left corner of the subfigure
4511 'subfigure pixels' pixels from the lower left corner of the subfigure
4512 'subfigure fraction' fraction of the subfigure, 0, 0 is lower left.
4513 'axes points' points from lower left corner of the Axes
4514 'axes pixels' pixels from lower left corner of the Axes
4515 'axes fraction' 0, 0 is lower left of Axes and 1, 1 is upper right
4516 'data' use the coordinate system of the object being
4517 annotated (default)
4518 'offset points' offset (in points) from the *xy* value
4519 'polar' you can specify *theta*, *r* for the annotation,
4520 even in cartesian plots. Note that if you are
4521 using a polar Axes, you do not need to specify
4522 polar for the coordinate system since that is the
4523 native "data" coordinate system.
4524 ==================== ==================================================
4525
4526 Alternatively they can be set to any valid
4527 `~matplotlib.transforms.Transform`.
4528
4529 Note that 'subfigure pixels' and 'figure pixels' are the same
4530 for the parent figure, so users who want code that is usable in
4531 a subfigure can use 'subfigure pixels'.
4532
4533 .. note::
4534
4535 Using `ConnectionPatch` across two `~.axes.Axes` instances
4536 is not directly compatible with :ref:`constrained layout
4537 <constrainedlayout_guide>`. Add the artist
4538 directly to the `.Figure` instead of adding it to a specific Axes,
4539 or exclude it from the layout using ``con.set_in_layout(False)``.
4540
4541 .. code-block:: default
4542
4543 fig, ax = plt.subplots(1, 2, constrained_layout=True)
4544 con = ConnectionPatch(..., axesA=ax[0], axesB=ax[1])
4545 fig.add_artist(con)
4546
4547 """
4548 if coordsB is None:
4549 coordsB = coordsA
4550 # we'll draw ourself after the artist we annotate by default
4551 self.xy1 = xyA
4552 self.xy2 = xyB
4553 self.coords1 = coordsA
4554 self.coords2 = coordsB
4555
4556 self.axesA = axesA
4557 self.axesB = axesB
4558
4559 super().__init__(posA=(0, 0), posB=(1, 1),
4560 arrowstyle=arrowstyle,
4561 connectionstyle=connectionstyle,
4562 patchA=patchA, patchB=patchB,
4563 shrinkA=shrinkA, shrinkB=shrinkB,
4564 mutation_scale=mutation_scale,
4565 mutation_aspect=mutation_aspect,
4566 clip_on=clip_on,
4567 **kwargs)
4568 # if True, draw annotation only if self.xy is inside the Axes
4569 self._annotation_clip = None
4570
4571 def _get_xy(self, xy, s, axes=None):
4572 """Calculate the pixel position of given point."""
4573 s0 = s # For the error message, if needed.
4574 if axes is None:
4575 axes = self.axes
4576 xy = np.array(xy)
4577 if s in ["figure points", "axes points"]:
4578 xy *= self.figure.dpi / 72
4579 s = s.replace("points", "pixels")
4580 elif s == "figure fraction":
4581 s = self.figure.transFigure
4582 elif s == "subfigure fraction":
4583 s = self.figure.transSubfigure
4584 elif s == "axes fraction":
4585 s = axes.transAxes
4586 x, y = xy
4587
4588 if s == 'data':
4589 trans = axes.transData
4590 x = float(self.convert_xunits(x))
4591 y = float(self.convert_yunits(y))
4592 return trans.transform((x, y))
4593 elif s == 'offset points':
4594 if self.xycoords == 'offset points': # prevent recursion
4595 return self._get_xy(self.xy, 'data')
4596 return (
4597 self._get_xy(self.xy, self.xycoords) # converted data point
4598 + xy * self.figure.dpi / 72) # converted offset
4599 elif s == 'polar':
4600 theta, r = x, y
4601 x = r * np.cos(theta)
4602 y = r * np.sin(theta)
4603 trans = axes.transData
4604 return trans.transform((x, y))
4605 elif s == 'figure pixels':
4606 # pixels from the lower left corner of the figure
4607 bb = self.figure.figbbox
4608 x = bb.x0 + x if x >= 0 else bb.x1 + x
4609 y = bb.y0 + y if y >= 0 else bb.y1 + y
4610 return x, y
4611 elif s == 'subfigure pixels':
4612 # pixels from the lower left corner of the figure
4613 bb = self.figure.bbox
4614 x = bb.x0 + x if x >= 0 else bb.x1 + x
4615 y = bb.y0 + y if y >= 0 else bb.y1 + y
4616 return x, y
4617 elif s == 'axes pixels':
4618 # pixels from the lower left corner of the Axes
4619 bb = axes.bbox
4620 x = bb.x0 + x if x >= 0 else bb.x1 + x
4621 y = bb.y0 + y if y >= 0 else bb.y1 + y
4622 return x, y
4623 elif isinstance(s, transforms.Transform):
4624 return s.transform(xy)
4625 else:
4626 raise ValueError(f"{s0} is not a valid coordinate transformation")
4627
4628 def set_annotation_clip(self, b):
4629 """
4630 Set the annotation's clipping behavior.
4631
4632 Parameters
4633 ----------
4634 b : bool or None
4635 - True: The annotation will be clipped when ``self.xy`` is
4636 outside the Axes.
4637 - False: The annotation will always be drawn.
4638 - None: The annotation will be clipped when ``self.xy`` is
4639 outside the Axes and ``self.xycoords == "data"``.
4640 """
4641 self._annotation_clip = b
4642 self.stale = True
4643
4644 def get_annotation_clip(self):
4645 """
4646 Return the clipping behavior.
4647
4648 See `.set_annotation_clip` for the meaning of the return value.
4649 """
4650 return self._annotation_clip
4651
4652 def _get_path_in_displaycoord(self):
4653 """Return the mutated path of the arrow in display coordinates."""
4654 dpi_cor = self._dpi_cor
4655 posA = self._get_xy(self.xy1, self.coords1, self.axesA)
4656 posB = self._get_xy(self.xy2, self.coords2, self.axesB)
4657 path = self.get_connectionstyle()(
4658 posA, posB,
4659 patchA=self.patchA, patchB=self.patchB,
4660 shrinkA=self.shrinkA * dpi_cor, shrinkB=self.shrinkB * dpi_cor,
4661 )
4662 path, fillable = self.get_arrowstyle()(
4663 path,
4664 self.get_mutation_scale() * dpi_cor,
4665 self.get_linewidth() * dpi_cor,
4666 self.get_mutation_aspect()
4667 )
4668 return path, fillable
4669
4670 def _check_xy(self, renderer):
4671 """Check whether the annotation needs to be drawn."""
4672
4673 b = self.get_annotation_clip()
4674
4675 if b or (b is None and self.coords1 == "data"):
4676 xy_pixel = self._get_xy(self.xy1, self.coords1, self.axesA)
4677 if self.axesA is None:
4678 axes = self.axes
4679 else:
4680 axes = self.axesA
4681 if not axes.contains_point(xy_pixel):
4682 return False
4683
4684 if b or (b is None and self.coords2 == "data"):
4685 xy_pixel = self._get_xy(self.xy2, self.coords2, self.axesB)
4686 if self.axesB is None:
4687 axes = self.axes
4688 else:
4689 axes = self.axesB
4690 if not axes.contains_point(xy_pixel):
4691 return False
4692
4693 return True
4694
4695 def draw(self, renderer):
4696 if not self.get_visible() or not self._check_xy(renderer):
4697 return
4698 super().draw(renderer)