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