Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/patches.py : 24%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import contextlib
2import functools
3import inspect
4import math
5from numbers import Number
6import textwrap
8import numpy as np
10import matplotlib as mpl
11from . import artist, cbook, colors, docstring, lines as mlines, transforms
12from .bezier import (
13 NonIntersectingPathException, concatenate_paths, get_cos_sin,
14 get_intersection, get_parallels, inside_circle, make_path_regular,
15 make_wedged_bezier2, split_bezier_intersecting_with_closedpath,
16 split_path_inout)
17from .path import Path
20@cbook._define_aliases({
21 "antialiased": ["aa"],
22 "edgecolor": ["ec"],
23 "facecolor": ["fc"],
24 "linestyle": ["ls"],
25 "linewidth": ["lw"],
26})
27class Patch(artist.Artist):
28 """
29 A patch is a 2D artist with a face color and an edge color.
31 If any of *edgecolor*, *facecolor*, *linewidth*, or *antialiased*
32 are *None*, they default to their rc params setting.
33 """
34 zorder = 1
35 validCap = ('butt', 'round', 'projecting')
36 validJoin = ('miter', 'round', 'bevel')
38 # Whether to draw an edge by default. Set on a
39 # subclass-by-subclass basis.
40 _edge_default = False
42 def __init__(self,
43 edgecolor=None,
44 facecolor=None,
45 color=None,
46 linewidth=None,
47 linestyle=None,
48 antialiased=None,
49 hatch=None,
50 fill=True,
51 capstyle=None,
52 joinstyle=None,
53 **kwargs):
54 """
55 The following kwarg properties are supported
57 %(Patch)s
58 """
59 artist.Artist.__init__(self)
61 if linewidth is None:
62 linewidth = mpl.rcParams['patch.linewidth']
63 if linestyle is None:
64 linestyle = "solid"
65 if capstyle is None:
66 capstyle = 'butt'
67 if joinstyle is None:
68 joinstyle = 'miter'
69 if antialiased is None:
70 antialiased = mpl.rcParams['patch.antialiased']
72 self._hatch_color = colors.to_rgba(mpl.rcParams['hatch.color'])
73 self._fill = True # needed for set_facecolor call
74 if color is not None:
75 if edgecolor is not None or facecolor is not None:
76 cbook._warn_external(
77 "Setting the 'color' property will override "
78 "the edgecolor or facecolor properties.")
79 self.set_color(color)
80 else:
81 self.set_edgecolor(edgecolor)
82 self.set_facecolor(facecolor)
83 # unscaled dashes. Needed to scale dash patterns by lw
84 self._us_dashes = None
85 self._linewidth = 0
87 self.set_fill(fill)
88 self.set_linestyle(linestyle)
89 self.set_linewidth(linewidth)
90 self.set_antialiased(antialiased)
91 self.set_hatch(hatch)
92 self.set_capstyle(capstyle)
93 self.set_joinstyle(joinstyle)
95 if len(kwargs):
96 self.update(kwargs)
98 def get_verts(self):
99 """
100 Return a copy of the vertices used in this patch.
102 If the patch contains Bezier curves, the curves will be
103 interpolated by line segments. To access the curves as
104 curves, use :meth:`get_path`.
105 """
106 trans = self.get_transform()
107 path = self.get_path()
108 polygons = path.to_polygons(trans)
109 if len(polygons):
110 return polygons[0]
111 return []
113 def _process_radius(self, radius):
114 if radius is not None:
115 return radius
116 if isinstance(self._picker, Number):
117 _radius = self._picker
118 else:
119 if self.get_edgecolor()[3] == 0:
120 _radius = 0
121 else:
122 _radius = self.get_linewidth()
123 return _radius
125 def contains(self, mouseevent, radius=None):
126 """
127 Test whether the mouse event occurred in the patch.
129 Returns
130 -------
131 (bool, empty dict)
132 """
133 inside, info = self._default_contains(mouseevent)
134 if inside is not None:
135 return inside, info
136 radius = self._process_radius(radius)
137 codes = self.get_path().codes
138 if codes is not None:
139 vertices = self.get_path().vertices
140 # if the current path is concatenated by multiple sub paths.
141 # get the indexes of the starting code(MOVETO) of all sub paths
142 idxs, = np.where(codes == Path.MOVETO)
143 # Don't split before the first MOVETO.
144 idxs = idxs[1:]
145 subpaths = map(
146 Path, np.split(vertices, idxs), np.split(codes, idxs))
147 else:
148 subpaths = [self.get_path()]
149 inside = any(
150 subpath.contains_point(
151 (mouseevent.x, mouseevent.y), self.get_transform(), radius)
152 for subpath in subpaths)
153 return inside, {}
155 def contains_point(self, point, radius=None):
156 """
157 Return whether the given point is inside the patch.
159 Parameters
160 ----------
161 point : (float, float)
162 The point (x, y) to check, in target coordinates of
163 ``self.get_transform()``. These are display coordinates for patches
164 that are added to a figure or axes.
165 radius : float, optional
166 Add an additional margin on the patch in target coordinates of
167 ``self.get_transform()``. See `.Path.contains_point` for further
168 details.
170 Returns
171 -------
172 bool
174 Notes
175 -----
176 The proper use of this method depends on the transform of the patch.
177 Isolated patches do not have a transform. In this case, the patch
178 creation coordinates and the point coordinates match. The following
179 example checks that the center of a circle is within the circle
181 >>> center = 0, 0
182 >>> c = Circle(center, radius=1)
183 >>> c.contains_point(center)
184 True
186 The convention of checking against the transformed patch stems from
187 the fact that this method is predominantly used to check if display
188 coordinates (e.g. from mouse events) are within the patch. If you want
189 to do the above check with data coordinates, you have to properly
190 transform them first:
192 >>> center = 0, 0
193 >>> c = Circle(center, radius=1)
194 >>> plt.gca().add_patch(c)
195 >>> transformed_center = c.get_transform().transform(center)
196 >>> c.contains_point(transformed_center)
197 True
199 """
200 radius = self._process_radius(radius)
201 return self.get_path().contains_point(point,
202 self.get_transform(),
203 radius)
205 def contains_points(self, points, radius=None):
206 """
207 Return whether the given points are inside the patch.
209 Parameters
210 ----------
211 points : (N, 2) array
212 The points to check, in target coordinates of
213 ``self.get_transform()``. These are display coordinates for patches
214 that are added to a figure or axes. Columns contain x and y values.
215 radius : float, optional
216 Add an additional margin on the patch in target coordinates of
217 ``self.get_transform()``. See `.Path.contains_point` for further
218 details.
220 Returns
221 -------
222 length-N bool array
224 Notes
225 -----
226 The proper use of this method depends on the transform of the patch.
227 See the notes on `.Patch.contains_point`.
228 """
229 radius = self._process_radius(radius)
230 return self.get_path().contains_points(points,
231 self.get_transform(),
232 radius)
234 def update_from(self, other):
235 """Updates this `.Patch` from the properties of *other*."""
236 artist.Artist.update_from(self, other)
237 # For some properties we don't need or don't want to go through the
238 # getters/setters, so we just copy them directly.
239 self._edgecolor = other._edgecolor
240 self._facecolor = other._facecolor
241 self._original_edgecolor = other._original_edgecolor
242 self._original_facecolor = other._original_facecolor
243 self._fill = other._fill
244 self._hatch = other._hatch
245 self._hatch_color = other._hatch_color
246 # copy the unscaled dash pattern
247 self._us_dashes = other._us_dashes
248 self.set_linewidth(other._linewidth) # also sets dash properties
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 :class:`~matplotlib.transforms.Transform` instance which
267 maps data coordinates to physical coordinates.
268 """
269 return artist.Artist.get_transform(self)
271 def get_patch_transform(self):
272 """
273 Return the :class:`~matplotlib.transforms.Transform` instance which
274 takes patch coordinates 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 b : 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 or 'auto'
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 if w is None:
393 w = mpl.rcParams['axes.linewidth']
395 self._linewidth = float(w)
396 # scale the dash pattern by the linewidth
397 offset, ls = self._us_dashes
398 self._dashoffset, self._dashes = mlines._scale_dashes(
399 offset, ls, self._linewidth)
400 self.stale = True
402 def set_linestyle(self, ls):
403 """
404 Set the patch linestyle.
406 =========================== =================
407 linestyle description
408 =========================== =================
409 ``'-'`` or ``'solid'`` solid line
410 ``'--'`` or ``'dashed'`` dashed line
411 ``'-.'`` or ``'dashdot'`` dash-dotted line
412 ``':'`` or ``'dotted'`` dotted line
413 =========================== =================
415 Alternatively a dash tuple of the following form can be provided::
417 (offset, onoffseq)
419 where ``onoffseq`` is an even length tuple of on and off ink in points.
421 Parameters
422 ----------
423 ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...}
424 The line style.
425 """
426 if ls is None:
427 ls = "solid"
428 self._linestyle = ls
429 # get the unscaled dash pattern
430 offset, ls = self._us_dashes = mlines._get_dash_pattern(ls)
431 # scale the dash pattern by the linewidth
432 self._dashoffset, self._dashes = mlines._scale_dashes(
433 offset, ls, self._linewidth)
434 self.stale = True
436 def set_fill(self, b):
437 """
438 Set whether to fill the patch.
440 Parameters
441 ----------
442 b : bool
443 """
444 self._fill = bool(b)
445 self._set_facecolor(self._original_facecolor)
446 self._set_edgecolor(self._original_edgecolor)
447 self.stale = True
449 def get_fill(self):
450 """Return whether the patch is filled."""
451 return self._fill
453 # Make fill a property so as to preserve the long-standing
454 # but somewhat inconsistent behavior in which fill was an
455 # attribute.
456 fill = property(get_fill, set_fill)
458 def set_capstyle(self, s):
459 """
460 Set the capstyle.
462 Parameters
463 ----------
464 s : {'butt', 'round', 'projecting'}
465 """
466 s = s.lower()
467 cbook._check_in_list(self.validCap, capstyle=s)
468 self._capstyle = s
469 self.stale = True
471 def get_capstyle(self):
472 """Return the capstyle."""
473 return self._capstyle
475 def set_joinstyle(self, s):
476 """Set the joinstyle.
478 Parameters
479 ----------
480 s : {'miter', 'round', 'bevel'}
481 """
482 s = s.lower()
483 cbook._check_in_list(self.validJoin, joinstyle=s)
484 self._joinstyle = s
485 self.stale = True
487 def get_joinstyle(self):
488 """Return the joinstyle."""
489 return self._joinstyle
491 def set_hatch(self, hatch):
492 r"""
493 Set the hatching pattern.
495 *hatch* can be one of::
497 / - diagonal hatching
498 \ - back diagonal
499 | - vertical
500 - - horizontal
501 + - crossed
502 x - crossed diagonal
503 o - small circle
504 O - large circle
505 . - dots
506 * - stars
508 Letters can be combined, in which case all the specified
509 hatchings are done. If same letter repeats, it increases the
510 density of hatching of that pattern.
512 Hatching is supported in the PostScript, PDF, SVG and Agg
513 backends only.
515 Parameters
516 ----------
517 hatch : {'/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'}
518 """
519 self._hatch = hatch
520 self.stale = True
522 def get_hatch(self):
523 """Return the hatching pattern."""
524 return self._hatch
526 @contextlib.contextmanager
527 def _bind_draw_path_function(self, renderer):
528 """
529 ``draw()`` helper factored out for sharing with `FancyArrowPatch`.
531 Yields a callable ``dp`` such that calling ``dp(*args, **kwargs)`` is
532 equivalent to calling ``renderer1.draw_path(gc, *args, **kwargs)``
533 where ``renderer1`` and ``gc`` have been suitably set from ``renderer``
534 and the artist's properties.
535 """
537 renderer.open_group('patch', self.get_gid())
538 gc = renderer.new_gc()
540 gc.set_foreground(self._edgecolor, isRGBA=True)
542 lw = self._linewidth
543 if self._edgecolor[3] == 0:
544 lw = 0
545 gc.set_linewidth(lw)
546 gc.set_dashes(self._dashoffset, self._dashes)
547 gc.set_capstyle(self._capstyle)
548 gc.set_joinstyle(self._joinstyle)
550 gc.set_antialiased(self._antialiased)
551 self._set_gc_clip(gc)
552 gc.set_url(self._url)
553 gc.set_snap(self.get_snap())
555 gc.set_alpha(self._alpha)
557 if self._hatch:
558 gc.set_hatch(self._hatch)
559 try:
560 gc.set_hatch_color(self._hatch_color)
561 except AttributeError:
562 # if we end up with a GC that does not have this method
563 cbook.warn_deprecated(
564 "3.1", message="Your backend does not support setting the "
565 "hatch color; such backends will become unsupported in "
566 "Matplotlib 3.3.")
568 if self.get_sketch_params() is not None:
569 gc.set_sketch_params(*self.get_sketch_params())
571 if self.get_path_effects():
572 from matplotlib.patheffects import PathEffectRenderer
573 renderer = PathEffectRenderer(self.get_path_effects(), renderer)
575 # In `with _bind_draw_path_function(renderer) as draw_path: ...`
576 # (in the implementations of `draw()` below), calls to `draw_path(...)`
577 # will occur as if they took place here with `gc` inserted as
578 # additional first argument.
579 yield functools.partial(renderer.draw_path, gc)
581 gc.restore()
582 renderer.close_group('patch')
583 self.stale = False
585 @artist.allow_rasterization
586 def draw(self, renderer):
587 """Draw to the given *renderer*."""
588 if not self.get_visible():
589 return
591 # Patch has traditionally ignored the dashoffset.
592 with cbook._setattr_cm(self, _dashoffset=0), \
593 self._bind_draw_path_function(renderer) as draw_path:
594 path = self.get_path()
595 transform = self.get_transform()
596 tpath = transform.transform_path_non_affine(path)
597 affine = transform.get_affine()
598 draw_path(tpath, affine,
599 # Work around a bug in the PDF and SVG renderers, which
600 # do not draw the hatches if the facecolor is fully
601 # transparent, but do if it is None.
602 self._facecolor if self._facecolor[3] else None)
604 def get_path(self):
605 """Return the path of this patch."""
606 raise NotImplementedError('Derived must override')
608 def get_window_extent(self, renderer=None):
609 return self.get_path().get_extents(self.get_transform())
611 def _convert_xy_units(self, xy):
612 """Convert x and y units for a tuple (x, y)."""
613 x = self.convert_xunits(xy[0])
614 y = self.convert_yunits(xy[1])
615 return x, y
618patchdoc = artist.kwdoc(Patch)
619for k in ['Rectangle', 'Circle', 'RegularPolygon', 'Polygon', 'Wedge', 'Arrow',
620 'FancyArrow', 'CirclePolygon', 'Ellipse', 'Arc', 'FancyBboxPatch',
621 'Patch']:
622 docstring.interpd.update({k: patchdoc})
624# define Patch.__init__ docstring after the class has been added to interpd
625docstring.dedent_interpd(Patch.__init__)
628class Shadow(Patch):
629 def __str__(self):
630 return "Shadow(%s)" % (str(self.patch))
632 @docstring.dedent_interpd
633 def __init__(self, patch, ox, oy, props=None, **kwargs):
634 """
635 Create a shadow of the given *patch* offset by *ox*, *oy*.
636 *props*, if not *None*, is a patch property update dictionary.
637 If *None*, the shadow will have have the same color as the face,
638 but darkened.
640 Valid keyword arguments are:
642 %(Patch)s
643 """
644 Patch.__init__(self)
645 self.patch = patch
646 self.props = props
647 self._ox, self._oy = ox, oy
648 self._shadow_transform = transforms.Affine2D()
649 self._update()
651 def _update(self):
652 self.update_from(self.patch)
654 # Place the shadow patch directly behind the inherited patch.
655 self.set_zorder(np.nextafter(self.patch.zorder, -np.inf))
657 if self.props is not None:
658 self.update(self.props)
659 else:
660 color = .3 * np.asarray(colors.to_rgb(self.patch.get_facecolor()))
661 self.set_facecolor(color)
662 self.set_edgecolor(color)
663 self.set_alpha(0.5)
665 def _update_transform(self, renderer):
666 ox = renderer.points_to_pixels(self._ox)
667 oy = renderer.points_to_pixels(self._oy)
668 self._shadow_transform.clear().translate(ox, oy)
670 def _get_ox(self):
671 return self._ox
673 def _set_ox(self, ox):
674 self._ox = ox
676 def _get_oy(self):
677 return self._oy
679 def _set_oy(self, oy):
680 self._oy = oy
682 def get_path(self):
683 return self.patch.get_path()
685 def get_patch_transform(self):
686 return self.patch.get_patch_transform() + self._shadow_transform
688 def draw(self, renderer):
689 self._update_transform(renderer)
690 Patch.draw(self, renderer)
693class Rectangle(Patch):
694 """
695 A rectangle with lower left at *xy* = (*x*, *y*) with
696 specified *width*, *height* and rotation *angle*.
697 """
699 def __str__(self):
700 pars = self._x0, self._y0, self._width, self._height, self.angle
701 fmt = "Rectangle(xy=(%g, %g), width=%g, height=%g, angle=%g)"
702 return fmt % pars
704 @docstring.dedent_interpd
705 def __init__(self, xy, width, height, angle=0.0, **kwargs):
706 """
707 Parameters
708 ----------
709 xy : (float, float)
710 The bottom and left rectangle coordinates
711 width : float
712 Rectangle width
713 height : float
714 Rectangle height
715 angle : float, optional
716 rotation in degrees anti-clockwise about *xy* (default is 0.0)
717 fill : bool, optional
718 Whether to fill the rectangle (default is ``True``)
720 Notes
721 -----
722 Valid keyword arguments are:
724 %(Patch)s
725 """
727 Patch.__init__(self, **kwargs)
729 self._x0 = xy[0]
730 self._y0 = xy[1]
732 self._width = width
733 self._height = height
735 self._x1 = self._x0 + self._width
736 self._y1 = self._y0 + self._height
738 self.angle = float(angle)
739 # Note: This cannot be calculated until this is added to an Axes
740 self._rect_transform = transforms.IdentityTransform()
742 def get_path(self):
743 """Return the vertices of the rectangle."""
744 return Path.unit_rectangle()
746 def _update_patch_transform(self):
747 """
748 Notes
749 -----
750 This cannot be called until after this has been added to an Axes,
751 otherwise unit conversion will fail. This makes it very important to
752 call the accessor method and not directly access the transformation
753 member variable.
754 """
755 x0, y0, x1, y1 = self._convert_units()
756 bbox = transforms.Bbox.from_extents(x0, y0, x1, y1)
757 rot_trans = transforms.Affine2D()
758 rot_trans.rotate_deg_around(x0, y0, self.angle)
759 self._rect_transform = transforms.BboxTransformTo(bbox)
760 self._rect_transform += rot_trans
762 def _update_x1(self):
763 self._x1 = self._x0 + self._width
765 def _update_y1(self):
766 self._y1 = self._y0 + self._height
768 def _convert_units(self):
769 """Convert bounds of the rectangle."""
770 x0 = self.convert_xunits(self._x0)
771 y0 = self.convert_yunits(self._y0)
772 x1 = self.convert_xunits(self._x1)
773 y1 = self.convert_yunits(self._y1)
774 return x0, y0, x1, y1
776 def get_patch_transform(self):
777 self._update_patch_transform()
778 return self._rect_transform
780 def get_x(self):
781 """Return the left coordinate of the rectangle."""
782 return self._x0
784 def get_y(self):
785 """Return the bottom coordinate of the rectangle."""
786 return self._y0
788 def get_xy(self):
789 """Return the left and bottom coords of the rectangle as a tuple."""
790 return self._x0, self._y0
792 def get_width(self):
793 """Return the width of the rectangle."""
794 return self._width
796 def get_height(self):
797 """Return the height of the rectangle."""
798 return self._height
800 def set_x(self, x):
801 """Set the left coordinate of the rectangle."""
802 self._x0 = x
803 self._update_x1()
804 self.stale = True
806 def set_y(self, y):
807 """Set the bottom coordinate of the rectangle."""
808 self._y0 = y
809 self._update_y1()
810 self.stale = True
812 def set_xy(self, xy):
813 """
814 Set the left and bottom coordinates of the rectangle.
816 Parameters
817 ----------
818 xy : (float, float)
819 """
820 self._x0, self._y0 = xy
821 self._update_x1()
822 self._update_y1()
823 self.stale = True
825 def set_width(self, w):
826 """Set the width of the rectangle."""
827 self._width = w
828 self._update_x1()
829 self.stale = True
831 def set_height(self, h):
832 """Set the height of the rectangle."""
833 self._height = h
834 self._update_y1()
835 self.stale = True
837 def set_bounds(self, *args):
838 """
839 Set the bounds of the rectangle as *left*, *bottom*, *width*, *height*.
841 The values may be passed as separate parameters or as a tuple::
843 set_bounds(left, bottom, width, height)
844 set_bounds((left, bottom, width, height))
846 .. ACCEPTS: (left, bottom, width, height)
847 """
848 if len(args) == 1:
849 l, b, w, h = args[0]
850 else:
851 l, b, w, h = args
852 self._x0 = l
853 self._y0 = b
854 self._width = w
855 self._height = h
856 self._update_x1()
857 self._update_y1()
858 self.stale = True
860 def get_bbox(self):
861 """Return the `.Bbox`."""
862 x0, y0, x1, y1 = self._convert_units()
863 return transforms.Bbox.from_extents(x0, y0, x1, y1)
865 xy = property(get_xy, set_xy)
868class RegularPolygon(Patch):
869 """
870 A regular polygon patch.
871 """
872 def __str__(self):
873 s = "RegularPolygon((%g, %g), %d, radius=%g, orientation=%g)"
874 return s % (self._xy[0], self._xy[1], self._numVertices, self._radius,
875 self._orientation)
877 @docstring.dedent_interpd
878 def __init__(self, xy, numVertices, radius=5, orientation=0,
879 **kwargs):
880 """
881 Constructor arguments:
883 *xy*
884 A length 2 tuple (*x*, *y*) of the center.
886 *numVertices*
887 the number of vertices.
889 *radius*
890 The distance from the center to each of the vertices.
892 *orientation*
893 rotates the polygon (in radians).
895 Valid keyword arguments are:
897 %(Patch)s
898 """
899 self._xy = xy
900 self._numVertices = numVertices
901 self._orientation = orientation
902 self._radius = radius
903 self._path = Path.unit_regular_polygon(numVertices)
904 self._poly_transform = transforms.Affine2D()
905 self._update_transform()
907 Patch.__init__(self, **kwargs)
909 def _update_transform(self):
910 self._poly_transform.clear() \
911 .scale(self.radius) \
912 .rotate(self.orientation) \
913 .translate(*self.xy)
915 @property
916 def xy(self):
917 return self._xy
919 @xy.setter
920 def xy(self, xy):
921 self._xy = xy
922 self._update_transform()
924 @property
925 def orientation(self):
926 return self._orientation
928 @orientation.setter
929 def orientation(self, orientation):
930 self._orientation = orientation
931 self._update_transform()
933 @property
934 def radius(self):
935 return self._radius
937 @radius.setter
938 def radius(self, radius):
939 self._radius = radius
940 self._update_transform()
942 @property
943 def numvertices(self):
944 return self._numVertices
946 @numvertices.setter
947 def numvertices(self, numVertices):
948 self._numVertices = numVertices
950 def get_path(self):
951 return self._path
953 def get_patch_transform(self):
954 self._update_transform()
955 return self._poly_transform
958class PathPatch(Patch):
959 """
960 A general polycurve path patch.
961 """
962 _edge_default = True
964 def __str__(self):
965 s = "PathPatch%d((%g, %g) ...)"
966 return s % (len(self._path.vertices), *tuple(self._path.vertices[0]))
968 @docstring.dedent_interpd
969 def __init__(self, path, **kwargs):
970 """
971 *path* is a :class:`matplotlib.path.Path` object.
973 Valid keyword arguments are:
975 %(Patch)s
976 """
977 Patch.__init__(self, **kwargs)
978 self._path = path
980 def get_path(self):
981 return self._path
983 def set_path(self, path):
984 self._path = path
987class Polygon(Patch):
988 """
989 A general polygon patch.
990 """
991 def __str__(self):
992 s = "Polygon%d((%g, %g) ...)"
993 return s % (len(self._path.vertices), *tuple(self._path.vertices[0]))
995 @docstring.dedent_interpd
996 def __init__(self, xy, closed=True, **kwargs):
997 """
998 *xy* is a numpy array with shape Nx2.
1000 If *closed* is *True*, the polygon will be closed so the
1001 starting and ending points are the same.
1003 Valid keyword arguments are:
1005 %(Patch)s
1006 """
1007 Patch.__init__(self, **kwargs)
1008 self._closed = closed
1009 self.set_xy(xy)
1011 def get_path(self):
1012 """
1013 Get the path of the polygon
1015 Returns
1016 -------
1017 path : Path
1018 The `~.path.Path` object for the polygon.
1019 """
1020 return self._path
1022 def get_closed(self):
1023 """
1024 Returns if the polygon is closed
1026 Returns
1027 -------
1028 closed : bool
1029 If the path is closed
1030 """
1031 return self._closed
1033 def set_closed(self, closed):
1034 """
1035 Set if the polygon is closed
1037 Parameters
1038 ----------
1039 closed : bool
1040 True if the polygon is closed
1041 """
1042 if self._closed == bool(closed):
1043 return
1044 self._closed = bool(closed)
1045 self.set_xy(self.get_xy())
1046 self.stale = True
1048 def get_xy(self):
1049 """
1050 Get the vertices of the path.
1052 Returns
1053 -------
1054 vertices : (N, 2) numpy array
1055 The coordinates of the vertices.
1056 """
1057 return self._path.vertices
1059 def set_xy(self, xy):
1060 """
1061 Set the vertices of the polygon.
1063 Parameters
1064 ----------
1065 xy : (N, 2) array-like
1066 The coordinates of the vertices.
1067 """
1068 xy = np.asarray(xy)
1069 if self._closed:
1070 if len(xy) and (xy[0] != xy[-1]).any():
1071 xy = np.concatenate([xy, [xy[0]]])
1072 else:
1073 if len(xy) > 2 and (xy[0] == xy[-1]).all():
1074 xy = xy[:-1]
1075 self._path = Path(xy, closed=self._closed)
1076 self.stale = True
1078 _get_xy = get_xy
1079 _set_xy = set_xy
1080 xy = property(get_xy, set_xy,
1081 doc='The vertices of the path as (N, 2) numpy array.')
1084class Wedge(Patch):
1085 """
1086 Wedge shaped patch.
1087 """
1088 def __str__(self):
1089 pars = (self.center[0], self.center[1], self.r,
1090 self.theta1, self.theta2, self.width)
1091 fmt = "Wedge(center=(%g, %g), r=%g, theta1=%g, theta2=%g, width=%s)"
1092 return fmt % pars
1094 @docstring.dedent_interpd
1095 def __init__(self, center, r, theta1, theta2, width=None, **kwargs):
1096 """
1097 A wedge centered at *x*, *y* center with radius *r* that
1098 sweeps *theta1* to *theta2* (in degrees). If *width* is given,
1099 then a partial wedge is drawn from inner radius *r* - *width*
1100 to outer radius *r*.
1102 Valid keyword arguments are:
1104 %(Patch)s
1105 """
1106 Patch.__init__(self, **kwargs)
1107 self.center = center
1108 self.r, self.width = r, width
1109 self.theta1, self.theta2 = theta1, theta2
1110 self._patch_transform = transforms.IdentityTransform()
1111 self._recompute_path()
1113 def _recompute_path(self):
1114 # Inner and outer rings are connected unless the annulus is complete
1115 if abs((self.theta2 - self.theta1) - 360) <= 1e-12:
1116 theta1, theta2 = 0, 360
1117 connector = Path.MOVETO
1118 else:
1119 theta1, theta2 = self.theta1, self.theta2
1120 connector = Path.LINETO
1122 # Form the outer ring
1123 arc = Path.arc(theta1, theta2)
1125 if self.width is not None:
1126 # Partial annulus needs to draw the outer ring
1127 # followed by a reversed and scaled inner ring
1128 v1 = arc.vertices
1129 v2 = arc.vertices[::-1] * (self.r - self.width) / self.r
1130 v = np.vstack([v1, v2, v1[0, :], (0, 0)])
1131 c = np.hstack([arc.codes, arc.codes, connector, Path.CLOSEPOLY])
1132 c[len(arc.codes)] = connector
1133 else:
1134 # Wedge doesn't need an inner ring
1135 v = np.vstack([arc.vertices, [(0, 0), arc.vertices[0, :], (0, 0)]])
1136 c = np.hstack([arc.codes, [connector, connector, Path.CLOSEPOLY]])
1138 # Shift and scale the wedge to the final location.
1139 v *= self.r
1140 v += np.asarray(self.center)
1141 self._path = Path(v, c)
1143 def set_center(self, center):
1144 self._path = None
1145 self.center = center
1146 self.stale = True
1148 def set_radius(self, radius):
1149 self._path = None
1150 self.r = radius
1151 self.stale = True
1153 def set_theta1(self, theta1):
1154 self._path = None
1155 self.theta1 = theta1
1156 self.stale = True
1158 def set_theta2(self, theta2):
1159 self._path = None
1160 self.theta2 = theta2
1161 self.stale = True
1163 def set_width(self, width):
1164 self._path = None
1165 self.width = width
1166 self.stale = True
1168 def get_path(self):
1169 if self._path is None:
1170 self._recompute_path()
1171 return self._path
1174# COVERAGE NOTE: Not used internally or from examples
1175class Arrow(Patch):
1176 """
1177 An arrow patch.
1178 """
1179 def __str__(self):
1180 return "Arrow()"
1182 _path = Path([[0.0, 0.1], [0.0, -0.1],
1183 [0.8, -0.1], [0.8, -0.3],
1184 [1.0, 0.0], [0.8, 0.3],
1185 [0.8, 0.1], [0.0, 0.1]],
1186 closed=True)
1188 @docstring.dedent_interpd
1189 def __init__(self, x, y, dx, dy, width=1.0, **kwargs):
1190 """
1191 Draws an arrow from (*x*, *y*) to (*x* + *dx*, *y* + *dy*).
1192 The width of the arrow is scaled by *width*.
1194 Parameters
1195 ----------
1196 x : scalar
1197 x coordinate of the arrow tail
1198 y : scalar
1199 y coordinate of the arrow tail
1200 dx : scalar
1201 Arrow length in the x direction
1202 dy : scalar
1203 Arrow length in the y direction
1204 width : scalar, optional (default: 1)
1205 Scale factor for the width of the arrow. With a default value of
1206 1, the tail width is 0.2 and head width is 0.6.
1207 **kwargs
1208 Keyword arguments control the `Patch` properties:
1210 %(Patch)s
1212 See Also
1213 --------
1214 :class:`FancyArrow` :
1215 Patch that allows independent control of the head and tail
1216 properties
1217 """
1218 super().__init__(**kwargs)
1219 self._patch_transform = (
1220 transforms.Affine2D()
1221 .scale(np.hypot(dx, dy), width)
1222 .rotate(np.arctan2(dy, dx))
1223 .translate(x, y)
1224 .frozen())
1226 def get_path(self):
1227 return self._path
1229 def get_patch_transform(self):
1230 return self._patch_transform
1233class FancyArrow(Polygon):
1234 """
1235 Like Arrow, but lets you set head width and head height independently.
1236 """
1238 _edge_default = True
1240 def __str__(self):
1241 return "FancyArrow()"
1243 @docstring.dedent_interpd
1244 def __init__(self, x, y, dx, dy, width=0.001, length_includes_head=False,
1245 head_width=None, head_length=None, shape='full', overhang=0,
1246 head_starts_at_zero=False, **kwargs):
1247 """
1248 Constructor arguments
1249 *width*: float (default: 0.001)
1250 width of full arrow tail
1252 *length_includes_head*: bool (default: False)
1253 True if head is to be counted in calculating the length.
1255 *head_width*: float or None (default: 3*width)
1256 total width of the full arrow head
1258 *head_length*: float or None (default: 1.5 * head_width)
1259 length of arrow head
1261 *shape*: ['full', 'left', 'right'] (default: 'full')
1262 draw the left-half, right-half, or full arrow
1264 *overhang*: float (default: 0)
1265 fraction that the arrow is swept back (0 overhang means
1266 triangular shape). Can be negative or greater than one.
1268 *head_starts_at_zero*: bool (default: False)
1269 if True, the head starts being drawn at coordinate 0
1270 instead of ending at coordinate 0.
1272 Other valid kwargs (inherited from :class:`Patch`) are:
1274 %(Patch)s
1275 """
1276 if head_width is None:
1277 head_width = 3 * width
1278 if head_length is None:
1279 head_length = 1.5 * head_width
1281 distance = np.hypot(dx, dy)
1283 if length_includes_head:
1284 length = distance
1285 else:
1286 length = distance + head_length
1287 if not length:
1288 verts = np.empty([0, 2]) # display nothing if empty
1289 else:
1290 # start by drawing horizontal arrow, point at (0, 0)
1291 hw, hl, hs, lw = head_width, head_length, overhang, width
1292 left_half_arrow = np.array([
1293 [0.0, 0.0], # tip
1294 [-hl, -hw / 2], # leftmost
1295 [-hl * (1 - hs), -lw / 2], # meets stem
1296 [-length, -lw / 2], # bottom left
1297 [-length, 0],
1298 ])
1299 # if we're not including the head, shift up by head length
1300 if not length_includes_head:
1301 left_half_arrow += [head_length, 0]
1302 # if the head starts at 0, shift up by another head length
1303 if head_starts_at_zero:
1304 left_half_arrow += [head_length / 2, 0]
1305 # figure out the shape, and complete accordingly
1306 if shape == 'left':
1307 coords = left_half_arrow
1308 else:
1309 right_half_arrow = left_half_arrow * [1, -1]
1310 if shape == 'right':
1311 coords = right_half_arrow
1312 elif shape == 'full':
1313 # The half-arrows contain the midpoint of the stem,
1314 # which we can omit from the full arrow. Including it
1315 # twice caused a problem with xpdf.
1316 coords = np.concatenate([left_half_arrow[:-1],
1317 right_half_arrow[-2::-1]])
1318 else:
1319 raise ValueError("Got unknown shape: %s" % shape)
1320 if distance != 0:
1321 cx = dx / distance
1322 sx = dy / distance
1323 else:
1324 # Account for division by zero
1325 cx, sx = 0, 1
1326 M = [[cx, sx], [-sx, cx]]
1327 verts = np.dot(coords, M) + (x + dx, y + dy)
1329 super().__init__(verts, closed=True, **kwargs)
1332docstring.interpd.update({"FancyArrow": FancyArrow.__init__.__doc__})
1335class CirclePolygon(RegularPolygon):
1336 """
1337 A polygon-approximation of a circle patch.
1338 """
1339 def __str__(self):
1340 s = "CirclePolygon((%g, %g), radius=%g, resolution=%d)"
1341 return s % (self._xy[0], self._xy[1], self._radius, self._numVertices)
1343 @docstring.dedent_interpd
1344 def __init__(self, xy, radius=5,
1345 resolution=20, # the number of vertices
1346 ** kwargs):
1347 """
1348 Create a circle at *xy* = (*x*, *y*) with given *radius*.
1349 This circle is approximated by a regular polygon with
1350 *resolution* sides. For a smoother circle drawn with splines,
1351 see :class:`~matplotlib.patches.Circle`.
1353 Valid keyword arguments are:
1355 %(Patch)s
1356 """
1357 RegularPolygon.__init__(self, xy,
1358 resolution,
1359 radius,
1360 orientation=0,
1361 **kwargs)
1364class Ellipse(Patch):
1365 """
1366 A scale-free ellipse.
1367 """
1368 def __str__(self):
1369 pars = (self._center[0], self._center[1],
1370 self.width, self.height, self.angle)
1371 fmt = "Ellipse(xy=(%s, %s), width=%s, height=%s, angle=%s)"
1372 return fmt % pars
1374 @docstring.dedent_interpd
1375 def __init__(self, xy, width, height, angle=0, **kwargs):
1376 """
1377 Parameters
1378 ----------
1379 xy : (float, float)
1380 xy coordinates of ellipse centre.
1381 width : float
1382 Total length (diameter) of horizontal axis.
1383 height : float
1384 Total length (diameter) of vertical axis.
1385 angle : scalar, optional
1386 Rotation in degrees anti-clockwise.
1388 Notes
1389 -----
1390 Valid keyword arguments are:
1392 %(Patch)s
1393 """
1394 Patch.__init__(self, **kwargs)
1396 self._center = xy
1397 self.width, self.height = width, height
1398 self.angle = angle
1399 self._path = Path.unit_circle()
1400 # Note: This cannot be calculated until this is added to an Axes
1401 self._patch_transform = transforms.IdentityTransform()
1403 def _recompute_transform(self):
1404 """
1405 Notes
1406 -----
1407 This cannot be called until after this has been added to an Axes,
1408 otherwise unit conversion will fail. This makes it very important to
1409 call the accessor method and not directly access the transformation
1410 member variable.
1411 """
1412 center = (self.convert_xunits(self._center[0]),
1413 self.convert_yunits(self._center[1]))
1414 width = self.convert_xunits(self.width)
1415 height = self.convert_yunits(self.height)
1416 self._patch_transform = transforms.Affine2D() \
1417 .scale(width * 0.5, height * 0.5) \
1418 .rotate_deg(self.angle) \
1419 .translate(*center)
1421 def get_path(self):
1422 """
1423 Return the path of the ellipse
1424 """
1425 return self._path
1427 def get_patch_transform(self):
1428 self._recompute_transform()
1429 return self._patch_transform
1431 def set_center(self, xy):
1432 """
1433 Set the center of the ellipse.
1435 Parameters
1436 ----------
1437 xy : (float, float)
1438 """
1439 self._center = xy
1440 self.stale = True
1442 def get_center(self):
1443 """
1444 Return the center of the ellipse
1445 """
1446 return self._center
1448 center = property(get_center, set_center)
1451class Circle(Ellipse):
1452 """
1453 A circle patch.
1454 """
1455 def __str__(self):
1456 pars = self.center[0], self.center[1], self.radius
1457 fmt = "Circle(xy=(%g, %g), radius=%g)"
1458 return fmt % pars
1460 @docstring.dedent_interpd
1461 def __init__(self, xy, radius=5, **kwargs):
1462 """
1463 Create true circle at center *xy* = (*x*, *y*) with given
1464 *radius*. Unlike :class:`~matplotlib.patches.CirclePolygon`
1465 which is a polygonal approximation, this uses Bezier splines
1466 and is much closer to a scale-free circle.
1468 Valid keyword arguments are:
1470 %(Patch)s
1471 """
1472 Ellipse.__init__(self, xy, radius * 2, radius * 2, **kwargs)
1473 self.radius = radius
1475 def set_radius(self, radius):
1476 """
1477 Set the radius of the circle
1479 Parameters
1480 ----------
1481 radius : float
1482 """
1483 self.width = self.height = 2 * radius
1484 self.stale = True
1486 def get_radius(self):
1487 """
1488 Return the radius of the circle
1489 """
1490 return self.width / 2.
1492 radius = property(get_radius, set_radius)
1495class Arc(Ellipse):
1496 """
1497 An elliptical arc, i.e. a segment of an ellipse.
1499 Due to internal optimizations, there are certain restrictions on using Arc:
1501 - The arc cannot be filled.
1503 - The arc must be used in an :class:`~.axes.Axes` instance---it can not be
1504 added directly to a `.Figure`---because it is optimized to only render
1505 the segments that are inside the axes bounding box with high resolution.
1506 """
1507 def __str__(self):
1508 pars = (self.center[0], self.center[1], self.width,
1509 self.height, self.angle, self.theta1, self.theta2)
1510 fmt = ("Arc(xy=(%g, %g), width=%g, "
1511 "height=%g, angle=%g, theta1=%g, theta2=%g)")
1512 return fmt % pars
1514 @docstring.dedent_interpd
1515 def __init__(self, xy, width, height, angle=0.0,
1516 theta1=0.0, theta2=360.0, **kwargs):
1517 """
1518 Parameters
1519 ----------
1520 xy : (float, float)
1521 The center of the ellipse.
1523 width : float
1524 The length of the horizontal axis.
1526 height : float
1527 The length of the vertical axis.
1529 angle : float
1530 Rotation of the ellipse in degrees (counterclockwise).
1532 theta1, theta2 : float, optional
1533 Starting and ending angles of the arc in degrees. These values
1534 are relative to *angle*, e.g. if *angle* = 45 and *theta1* = 90
1535 the absolute starting angle is 135.
1536 Default *theta1* = 0, *theta2* = 360, i.e. a complete ellipse.
1537 The arc is drawn in the counterclockwise direction.
1538 Angles greater than or equal to 360, or smaller than 0, are
1539 represented by an equivalent angle in the range [0, 360), by
1540 taking the input value mod 360.
1542 Other Parameters
1543 ----------------
1544 **kwargs : `.Patch` properties
1545 Most `.Patch` properties are supported as keyword arguments,
1546 with the exception of *fill* and *facecolor* because filling is
1547 not supported.
1549 %(Patch)s
1550 """
1551 fill = kwargs.setdefault('fill', False)
1552 if fill:
1553 raise ValueError("Arc objects can not be filled")
1555 Ellipse.__init__(self, xy, width, height, angle, **kwargs)
1557 self.theta1 = theta1
1558 self.theta2 = theta2
1560 @artist.allow_rasterization
1561 def draw(self, renderer):
1562 """
1563 Draw the arc to the given *renderer*.
1565 Notes
1566 -----
1567 Ellipses are normally drawn using an approximation that uses
1568 eight cubic Bezier splines. The error of this approximation
1569 is 1.89818e-6, according to this unverified source:
1571 Lancaster, Don. *Approximating a Circle or an Ellipse Using
1572 Four Bezier Cubic Splines.*
1574 http://www.tinaja.com/glib/ellipse4.pdf
1576 There is a use case where very large ellipses must be drawn
1577 with very high accuracy, and it is too expensive to render the
1578 entire ellipse with enough segments (either splines or line
1579 segments). Therefore, in the case where either radius of the
1580 ellipse is large enough that the error of the spline
1581 approximation will be visible (greater than one pixel offset
1582 from the ideal), a different technique is used.
1584 In that case, only the visible parts of the ellipse are drawn,
1585 with each visible arc using a fixed number of spline segments
1586 (8). The algorithm proceeds as follows:
1588 1. The points where the ellipse intersects the axes bounding
1589 box are located. (This is done be performing an inverse
1590 transformation on the axes bbox such that it is relative
1591 to the unit circle -- this makes the intersection
1592 calculation much easier than doing rotated ellipse
1593 intersection directly).
1595 This uses the "line intersecting a circle" algorithm from:
1597 Vince, John. *Geometry for Computer Graphics: Formulae,
1598 Examples & Proofs.* London: Springer-Verlag, 2005.
1600 2. The angles of each of the intersection points are calculated.
1602 3. Proceeding counterclockwise starting in the positive
1603 x-direction, each of the visible arc-segments between the
1604 pairs of vertices are drawn using the Bezier arc
1605 approximation technique implemented in
1606 :meth:`matplotlib.path.Path.arc`.
1607 """
1608 if not hasattr(self, 'axes'):
1609 raise RuntimeError('Arcs can only be used in Axes instances')
1610 if not self.get_visible():
1611 return
1613 self._recompute_transform()
1615 width = self.convert_xunits(self.width)
1616 height = self.convert_yunits(self.height)
1618 # If the width and height of ellipse are not equal, take into account
1619 # stretching when calculating angles to draw between
1620 def theta_stretch(theta, scale):
1621 theta = np.deg2rad(theta)
1622 x = np.cos(theta)
1623 y = np.sin(theta)
1624 stheta = np.rad2deg(np.arctan2(scale * y, x))
1625 # arctan2 has the range [-pi, pi], we expect [0, 2*pi]
1626 return (stheta + 360) % 360
1628 theta1 = self.theta1
1629 theta2 = self.theta2
1631 if (
1632 # if we need to stretch the angles because we are distorted
1633 width != height
1634 # and we are not doing a full circle.
1635 #
1636 # 0 and 360 do not exactly round-trip through the angle
1637 # stretching (due to both float precision limitations and
1638 # the difference between the range of arctan2 [-pi, pi] and
1639 # this method [0, 360]) so avoid doing it if we don't have to.
1640 and not (theta1 != theta2 and theta1 % 360 == theta2 % 360)
1641 ):
1642 theta1 = theta_stretch(self.theta1, width / height)
1643 theta2 = theta_stretch(self.theta2, width / height)
1645 # Get width and height in pixels we need to use
1646 # `self.get_data_transform` rather than `self.get_transform`
1647 # because we want the transform from dataspace to the
1648 # screen space to estimate how big the arc will be in physical
1649 # units when rendered (the transform that we get via
1650 # `self.get_transform()` goes from an idealized unit-radius
1651 # space to screen space).
1652 data_to_screen_trans = self.get_data_transform()
1653 pwidth, pheight = (data_to_screen_trans.transform((width, height)) -
1654 data_to_screen_trans.transform((0, 0)))
1655 inv_error = (1.0 / 1.89818e-6) * 0.5
1657 if pwidth < inv_error and pheight < inv_error:
1658 self._path = Path.arc(theta1, theta2)
1659 return Patch.draw(self, renderer)
1661 def line_circle_intersect(x0, y0, x1, y1):
1662 dx = x1 - x0
1663 dy = y1 - y0
1664 dr2 = dx * dx + dy * dy
1665 D = x0 * y1 - x1 * y0
1666 D2 = D * D
1667 discrim = dr2 - D2
1668 if discrim >= 0.0:
1669 sign_dy = np.copysign(1, dy) # +/-1, never 0.
1670 sqrt_discrim = np.sqrt(discrim)
1671 return np.array(
1672 [[(D * dy + sign_dy * dx * sqrt_discrim) / dr2,
1673 (-D * dx + abs(dy) * sqrt_discrim) / dr2],
1674 [(D * dy - sign_dy * dx * sqrt_discrim) / dr2,
1675 (-D * dx - abs(dy) * sqrt_discrim) / dr2]])
1676 else:
1677 return np.empty((0, 2))
1679 def segment_circle_intersect(x0, y0, x1, y1):
1680 epsilon = 1e-9
1681 if x1 < x0:
1682 x0e, x1e = x1, x0
1683 else:
1684 x0e, x1e = x0, x1
1685 if y1 < y0:
1686 y0e, y1e = y1, y0
1687 else:
1688 y0e, y1e = y0, y1
1689 xys = line_circle_intersect(x0, y0, x1, y1)
1690 xs, ys = xys.T
1691 return xys[
1692 (x0e - epsilon < xs) & (xs < x1e + epsilon)
1693 & (y0e - epsilon < ys) & (ys < y1e + epsilon)
1694 ]
1696 # Transforms the axes box_path so that it is relative to the unit
1697 # circle in the same way that it is relative to the desired ellipse.
1698 box_path_transform = (transforms.BboxTransformTo(self.axes.bbox)
1699 + self.get_transform().inverted())
1700 box_path = Path.unit_rectangle().transformed(box_path_transform)
1702 thetas = set()
1703 # For each of the point pairs, there is a line segment
1704 for p0, p1 in zip(box_path.vertices[:-1], box_path.vertices[1:]):
1705 xy = segment_circle_intersect(*p0, *p1)
1706 x, y = xy.T
1707 # arctan2 return [-pi, pi), the rest of our angles are in
1708 # [0, 360], adjust as needed.
1709 theta = (np.rad2deg(np.arctan2(y, x)) + 360) % 360
1710 thetas.update(theta[(theta1 < theta) & (theta < theta2)])
1711 thetas = sorted(thetas) + [theta2]
1712 last_theta = theta1
1713 theta1_rad = np.deg2rad(theta1)
1714 inside = box_path.contains_point(
1715 (np.cos(theta1_rad), np.sin(theta1_rad))
1716 )
1718 # save original path
1719 path_original = self._path
1720 for theta in thetas:
1721 if inside:
1722 self._path = Path.arc(last_theta, theta, 8)
1723 Patch.draw(self, renderer)
1724 inside = False
1725 else:
1726 inside = True
1727 last_theta = theta
1729 # restore original path
1730 self._path = path_original
1733def bbox_artist(artist, renderer, props=None, fill=True):
1734 """
1735 This is a debug function to draw a rectangle around the bounding
1736 box returned by
1737 :meth:`~matplotlib.artist.Artist.get_window_extent` of an artist,
1738 to test whether the artist is returning the correct bbox.
1740 *props* is a dict of rectangle props with the additional property
1741 'pad' that sets the padding around the bbox in points.
1742 """
1743 if props is None:
1744 props = {}
1745 props = props.copy() # don't want to alter the pad externally
1746 pad = props.pop('pad', 4)
1747 pad = renderer.points_to_pixels(pad)
1748 bbox = artist.get_window_extent(renderer)
1749 l, b, w, h = bbox.bounds
1750 l -= pad / 2.
1751 b -= pad / 2.
1752 w += pad
1753 h += pad
1754 r = Rectangle(xy=(l, b),
1755 width=w,
1756 height=h,
1757 fill=fill,
1758 )
1759 r.set_transform(transforms.IdentityTransform())
1760 r.set_clip_on(False)
1761 r.update(props)
1762 r.draw(renderer)
1765def draw_bbox(bbox, renderer, color='k', trans=None):
1766 """
1767 This is a debug function to draw a rectangle around the bounding
1768 box returned by
1769 :meth:`~matplotlib.artist.Artist.get_window_extent` of an artist,
1770 to test whether the artist is returning the correct bbox.
1771 """
1773 l, b, w, h = bbox.bounds
1774 r = Rectangle(xy=(l, b),
1775 width=w,
1776 height=h,
1777 edgecolor=color,
1778 fill=False,
1779 )
1780 if trans is not None:
1781 r.set_transform(trans)
1782 r.set_clip_on(False)
1783 r.draw(renderer)
1786def _pprint_styles(_styles):
1787 """
1788 A helper function for the _Style class. Given the dictionary of
1789 {stylename: styleclass}, return a formatted string listing all the
1790 styles. Used to update the documentation.
1791 """
1792 table = [('Class', 'Name', 'Attrs'),
1793 *[(cls.__name__,
1794 # adding backquotes since - and | have special meaning in reST
1795 f'``{name}``',
1796 # [1:-1] drops the surrounding parentheses.
1797 str(inspect.signature(cls))[1:-1] or 'None')
1798 for name, cls in sorted(_styles.items())]]
1799 # Convert to rst table.
1800 col_len = [max(len(cell) for cell in column) for column in zip(*table)]
1801 table_formatstr = ' '.join('=' * cl for cl in col_len)
1802 rst_table = '\n'.join([
1803 '',
1804 table_formatstr,
1805 ' '.join(cell.ljust(cl) for cell, cl in zip(table[0], col_len)),
1806 table_formatstr,
1807 *[' '.join(cell.ljust(cl) for cell, cl in zip(row, col_len))
1808 for row in table[1:]],
1809 table_formatstr,
1810 '',
1811 ])
1812 return textwrap.indent(rst_table, prefix=' ' * 2)
1815def _simpleprint_styles(_styles):
1816 """
1817 A helper function for the _Style class. Given the dictionary of
1818 {stylename: styleclass}, return a string rep of the list of keys.
1819 Used to update the documentation.
1820 """
1821 return "[{}]".format("|".join(map(" '{}' ".format, sorted(_styles))))
1824class _Style:
1825 """
1826 A base class for the Styles. It is meant to be a container class,
1827 where actual styles are declared as subclass of it, and it
1828 provides some helper functions.
1829 """
1830 def __new__(cls, stylename, **kw):
1831 """Return the instance of the subclass with the given style name."""
1833 # The "class" should have the _style_list attribute, which is a mapping
1834 # of style names to style classes.
1836 _list = stylename.replace(" ", "").split(",")
1837 _name = _list[0].lower()
1838 try:
1839 _cls = cls._style_list[_name]
1840 except KeyError:
1841 raise ValueError("Unknown style : %s" % stylename)
1843 try:
1844 _args_pair = [cs.split("=") for cs in _list[1:]]
1845 _args = {k: float(v) for k, v in _args_pair}
1846 except ValueError:
1847 raise ValueError("Incorrect style argument : %s" % stylename)
1848 _args.update(kw)
1850 return _cls(**_args)
1852 @classmethod
1853 def get_styles(cls):
1854 """
1855 A class method which returns a dictionary of available styles.
1856 """
1857 return cls._style_list
1859 @classmethod
1860 def pprint_styles(cls):
1861 """
1862 A class method which returns a string of the available styles.
1863 """
1864 return _pprint_styles(cls._style_list)
1866 @classmethod
1867 def register(cls, name, style):
1868 """
1869 Register a new style.
1870 """
1872 if not issubclass(style, cls._Base):
1873 raise ValueError("%s must be a subclass of %s" % (style,
1874 cls._Base))
1875 cls._style_list[name] = style
1878def _register_style(style_list, cls=None, *, name=None):
1879 """Class decorator that stashes a class in a (style) dictionary."""
1880 if cls is None:
1881 return functools.partial(_register_style, style_list, name=name)
1882 style_list[name or cls.__name__.lower()] = cls
1883 return cls
1886class BoxStyle(_Style):
1887 """
1888 :class:`BoxStyle` is a container class which defines several
1889 boxstyle classes, which are used for :class:`FancyBboxPatch`.
1891 A style object can be created as::
1893 BoxStyle.Round(pad=0.2)
1895 or::
1897 BoxStyle("Round", pad=0.2)
1899 or::
1901 BoxStyle("Round, pad=0.2")
1903 Following boxstyle classes are defined.
1905 %(AvailableBoxstyles)s
1907 An instance of any boxstyle class is an callable object,
1908 whose call signature is::
1910 __call__(self, x0, y0, width, height, mutation_size, aspect_ratio=1.)
1912 and returns a :class:`Path` instance. *x0*, *y0*, *width* and
1913 *height* specify the location and size of the box to be
1914 drawn. *mutation_scale* determines the overall size of the
1915 mutation (by which I mean the transformation of the rectangle to
1916 the fancy box). *mutation_aspect* determines the aspect-ratio of
1917 the mutation.
1918 """
1920 _style_list = {}
1922 class _Base:
1923 """
1924 Abstract base class for styling of `.FancyBboxPatch`.
1926 This class is not an artist itself. The `__call__` method returns the
1927 `~matplotlib.path.Path` for outlining the fancy box. The actual drawing
1928 is handled in `.FancyBboxPatch`.
1930 Subclasses may only use parameters with default values in their
1931 ``__init__`` method because they must be able to be initialized
1932 without arguments.
1934 Subclasses must implement the `transmute` method. It receives the
1935 enclosing rectangle *x0, y0, width, height* as well as the
1936 *mutation_size*, which scales the outline properties such as padding.
1937 It returns the outline of the fancy box as `.path.Path`.
1938 """
1940 def transmute(self, x0, y0, width, height, mutation_size):
1941 """Return the `~.path.Path` outlining the given rectangle."""
1942 raise NotImplementedError('Derived must override')
1944 def __call__(self, x0, y0, width, height, mutation_size,
1945 aspect_ratio=1.):
1946 """
1947 Given the location and size of the box, return the path of
1948 the box around it.
1950 Parameters
1951 ----------
1952 x0, y0, width, height : float
1953 Location and size of the box.
1954 mutation_size : float
1955 A reference scale for the mutation.
1956 aspect_ratio : float, default: 1
1957 Aspect-ratio for the mutation.
1959 Returns
1960 -------
1961 path : `~matplotlib.path.Path`
1962 """
1963 # The __call__ method is a thin wrapper around the transmute method
1964 # and takes care of the aspect.
1966 if aspect_ratio is not None:
1967 # Squeeze the given height by the aspect_ratio
1968 y0, height = y0 / aspect_ratio, height / aspect_ratio
1969 # call transmute method with squeezed height.
1970 path = self.transmute(x0, y0, width, height, mutation_size)
1971 vertices, codes = path.vertices, path.codes
1972 # Restore the height
1973 vertices[:, 1] = vertices[:, 1] * aspect_ratio
1974 return Path(vertices, codes)
1975 else:
1976 return self.transmute(x0, y0, width, height, mutation_size)
1978 @_register_style(_style_list)
1979 class Square(_Base):
1980 """
1981 A square box.
1983 Parameters
1984 ----------
1985 pad : float, default: 0.3
1986 The amount of padding around the original box.
1987 """
1988 def __init__(self, pad=0.3):
1989 self.pad = pad
1990 super().__init__()
1992 def transmute(self, x0, y0, width, height, mutation_size):
1993 pad = mutation_size * self.pad
1995 # width and height with padding added.
1996 width, height = width + 2*pad, height + 2*pad
1998 # boundary of the padded box
1999 x0, y0 = x0 - pad, y0 - pad,
2000 x1, y1 = x0 + width, y0 + height
2002 vertices = [(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)]
2003 codes = [Path.MOVETO] + [Path.LINETO] * 3 + [Path.CLOSEPOLY]
2004 return Path(vertices, codes)
2006 @_register_style(_style_list)
2007 class Circle(_Base):
2008 """
2009 A circular box.
2011 Parameters
2012 ----------
2013 pad : float, default: 0.3
2014 The amount of padding around the original box.
2015 """
2016 def __init__(self, pad=0.3):
2017 self.pad = pad
2018 super().__init__()
2020 def transmute(self, x0, y0, width, height, mutation_size):
2021 pad = mutation_size * self.pad
2022 width, height = width + 2 * pad, height + 2 * pad
2024 # boundary of the padded box
2025 x0, y0 = x0 - pad, y0 - pad,
2026 return Path.circle((x0 + width / 2, y0 + height / 2),
2027 max(width, height) / 2)
2029 @_register_style(_style_list)
2030 class LArrow(_Base):
2031 """
2032 A box in the shape of a left-pointing arrow.
2034 Parameters
2035 ----------
2036 pad : float, default: 0.3
2037 The amount of padding around the original box.
2038 """
2039 def __init__(self, pad=0.3):
2040 self.pad = pad
2041 super().__init__()
2043 def transmute(self, x0, y0, width, height, mutation_size):
2044 # padding
2045 pad = mutation_size * self.pad
2047 # width and height with padding added.
2048 width, height = width + 2. * pad, height + 2. * pad
2050 # boundary of the padded box
2051 x0, y0 = x0 - pad, y0 - pad,
2052 x1, y1 = x0 + width, y0 + height
2054 dx = (y1 - y0) / 2.
2055 dxx = dx * .5
2056 # adjust x0. 1.4 <- sqrt(2)
2057 x0 = x0 + pad / 1.4
2059 cp = [(x0 + dxx, y0), (x1, y0), (x1, y1), (x0 + dxx, y1),
2060 (x0 + dxx, y1 + dxx), (x0 - dx, y0 + dx),
2061 (x0 + dxx, y0 - dxx), # arrow
2062 (x0 + dxx, y0), (x0 + dxx, y0)]
2064 com = [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO,
2065 Path.LINETO, Path.LINETO, Path.LINETO,
2066 Path.LINETO, Path.CLOSEPOLY]
2068 path = Path(cp, com)
2070 return path
2072 @_register_style(_style_list)
2073 class RArrow(LArrow):
2074 """
2075 A box in the shape of a right-pointing arrow.
2077 Parameters
2078 ----------
2079 pad : float, default: 0.3
2080 The amount of padding around the original box.
2081 """
2082 def __init__(self, pad=0.3):
2083 super().__init__(pad)
2085 def transmute(self, x0, y0, width, height, mutation_size):
2086 p = BoxStyle.LArrow.transmute(self, x0, y0,
2087 width, height, mutation_size)
2088 p.vertices[:, 0] = 2 * x0 + width - p.vertices[:, 0]
2089 return p
2091 @_register_style(_style_list)
2092 class DArrow(_Base):
2093 """
2094 A box in the shape of a two-way arrow.
2096 Parameters
2097 ----------
2098 pad : float, default: 0.3
2099 The amount of padding around the original box.
2100 """
2101 # This source is copied from LArrow,
2102 # modified to add a right arrow to the bbox.
2104 def __init__(self, pad=0.3):
2105 self.pad = pad
2106 super().__init__()
2108 def transmute(self, x0, y0, width, height, mutation_size):
2110 # padding
2111 pad = mutation_size * self.pad
2113 # width and height with padding added.
2114 # The width is padded by the arrows, so we don't need to pad it.
2115 height = height + 2. * pad
2117 # boundary of the padded box
2118 x0, y0 = x0 - pad, y0 - pad
2119 x1, y1 = x0 + width, y0 + height
2121 dx = (y1 - y0) / 2
2122 dxx = dx * .5
2123 # adjust x0. 1.4 <- sqrt(2)
2124 x0 = x0 + pad / 1.4
2126 cp = [(x0 + dxx, y0), (x1, y0), # bot-segment
2127 (x1, y0 - dxx), (x1 + dx + dxx, y0 + dx),
2128 (x1, y1 + dxx), # right-arrow
2129 (x1, y1), (x0 + dxx, y1), # top-segment
2130 (x0 + dxx, y1 + dxx), (x0 - dx, y0 + dx),
2131 (x0 + dxx, y0 - dxx), # left-arrow
2132 (x0 + dxx, y0), (x0 + dxx, y0)] # close-poly
2134 com = [Path.MOVETO, Path.LINETO,
2135 Path.LINETO, Path.LINETO,
2136 Path.LINETO,
2137 Path.LINETO, Path.LINETO,
2138 Path.LINETO, Path.LINETO,
2139 Path.LINETO,
2140 Path.LINETO, Path.CLOSEPOLY]
2142 path = Path(cp, com)
2144 return path
2146 @_register_style(_style_list)
2147 class Round(_Base):
2148 """
2149 A box with round corners.
2151 Parameters
2152 ----------
2153 pad : float, default: 0.3
2154 The amount of padding around the original box.
2155 rounding_size : float, default: *pad*
2156 Radius of the corners.
2157 """
2158 def __init__(self, pad=0.3, rounding_size=None):
2159 self.pad = pad
2160 self.rounding_size = rounding_size
2161 super().__init__()
2163 def transmute(self, x0, y0, width, height, mutation_size):
2165 # padding
2166 pad = mutation_size * self.pad
2168 # size of the rounding corner
2169 if self.rounding_size:
2170 dr = mutation_size * self.rounding_size
2171 else:
2172 dr = pad
2174 width, height = width + 2. * pad, height + 2. * pad
2176 x0, y0 = x0 - pad, y0 - pad,
2177 x1, y1 = x0 + width, y0 + height
2179 # Round corners are implemented as quadratic Bezier, e.g.,
2180 # [(x0, y0-dr), (x0, y0), (x0+dr, y0)] for lower left corner.
2181 cp = [(x0 + dr, y0),
2182 (x1 - dr, y0),
2183 (x1, y0), (x1, y0 + dr),
2184 (x1, y1 - dr),
2185 (x1, y1), (x1 - dr, y1),
2186 (x0 + dr, y1),
2187 (x0, y1), (x0, y1 - dr),
2188 (x0, y0 + dr),
2189 (x0, y0), (x0 + dr, y0),
2190 (x0 + dr, y0)]
2192 com = [Path.MOVETO,
2193 Path.LINETO,
2194 Path.CURVE3, Path.CURVE3,
2195 Path.LINETO,
2196 Path.CURVE3, Path.CURVE3,
2197 Path.LINETO,
2198 Path.CURVE3, Path.CURVE3,
2199 Path.LINETO,
2200 Path.CURVE3, Path.CURVE3,
2201 Path.CLOSEPOLY]
2203 path = Path(cp, com)
2205 return path
2207 @_register_style(_style_list)
2208 class Round4(_Base):
2209 """
2210 A box with rounded edges.
2212 Parameters
2213 ----------
2214 pad : float, default: 0.3
2215 The amount of padding around the original box.
2216 rounding_size : float, default: *pad*/2
2217 Rounding of edges.
2218 """
2219 def __init__(self, pad=0.3, rounding_size=None):
2220 self.pad = pad
2221 self.rounding_size = rounding_size
2222 super().__init__()
2224 def transmute(self, x0, y0, width, height, mutation_size):
2226 # padding
2227 pad = mutation_size * self.pad
2229 # Rounding size; defaults to half of the padding.
2230 if self.rounding_size:
2231 dr = mutation_size * self.rounding_size
2232 else:
2233 dr = pad / 2.
2235 width, height = (width + 2. * pad - 2 * dr,
2236 height + 2. * pad - 2 * dr)
2238 x0, y0 = x0 - pad + dr, y0 - pad + dr,
2239 x1, y1 = x0 + width, y0 + height
2241 cp = [(x0, y0),
2242 (x0 + dr, y0 - dr), (x1 - dr, y0 - dr), (x1, y0),
2243 (x1 + dr, y0 + dr), (x1 + dr, y1 - dr), (x1, y1),
2244 (x1 - dr, y1 + dr), (x0 + dr, y1 + dr), (x0, y1),
2245 (x0 - dr, y1 - dr), (x0 - dr, y0 + dr), (x0, y0),
2246 (x0, y0)]
2248 com = [Path.MOVETO,
2249 Path.CURVE4, Path.CURVE4, Path.CURVE4,
2250 Path.CURVE4, Path.CURVE4, Path.CURVE4,
2251 Path.CURVE4, Path.CURVE4, Path.CURVE4,
2252 Path.CURVE4, Path.CURVE4, Path.CURVE4,
2253 Path.CLOSEPOLY]
2255 path = Path(cp, com)
2257 return path
2259 @_register_style(_style_list)
2260 class Sawtooth(_Base):
2261 """
2262 A box with a sawtooth outline.
2264 Parameters
2265 ----------
2266 pad : float, default: 0.3
2267 The amount of padding around the original box.
2268 tooth_size : float, default: *pad*/2
2269 Size of the sawtooth.
2270 """
2271 def __init__(self, pad=0.3, tooth_size=None):
2272 self.pad = pad
2273 self.tooth_size = tooth_size
2274 super().__init__()
2276 def _get_sawtooth_vertices(self, x0, y0, width, height, mutation_size):
2278 # padding
2279 pad = mutation_size * self.pad
2281 # size of sawtooth
2282 if self.tooth_size is None:
2283 tooth_size = self.pad * .5 * mutation_size
2284 else:
2285 tooth_size = self.tooth_size * mutation_size
2287 tooth_size2 = tooth_size / 2.
2288 width, height = (width + 2. * pad - tooth_size,
2289 height + 2. * pad - tooth_size)
2291 # the sizes of the vertical and horizontal sawtooth are
2292 # separately adjusted to fit the given box size.
2293 dsx_n = int(round((width - tooth_size) / (tooth_size * 2))) * 2
2294 dsx = (width - tooth_size) / dsx_n
2295 dsy_n = int(round((height - tooth_size) / (tooth_size * 2))) * 2
2296 dsy = (height - tooth_size) / dsy_n
2298 x0, y0 = x0 - pad + tooth_size2, y0 - pad + tooth_size2
2299 x1, y1 = x0 + width, y0 + height
2301 bottom_saw_x = [
2302 x0,
2303 *(x0 + tooth_size2 + dsx * .5 * np.arange(dsx_n * 2)),
2304 x1 - tooth_size2,
2305 ]
2306 bottom_saw_y = [
2307 y0,
2308 *([y0 - tooth_size2, y0, y0 + tooth_size2, y0] * dsx_n),
2309 y0 - tooth_size2,
2310 ]
2311 right_saw_x = [
2312 x1,
2313 *([x1 + tooth_size2, x1, x1 - tooth_size2, x1] * dsx_n),
2314 x1 + tooth_size2,
2315 ]
2316 right_saw_y = [
2317 y0,
2318 *(y0 + tooth_size2 + dsy * .5 * np.arange(dsy_n * 2)),
2319 y1 - tooth_size2,
2320 ]
2321 top_saw_x = [
2322 x1,
2323 *(x1 - tooth_size2 - dsx * .5 * np.arange(dsx_n * 2)),
2324 x0 + tooth_size2,
2325 ]
2326 top_saw_y = [
2327 y1,
2328 *([y1 + tooth_size2, y1, y1 - tooth_size2, y1] * dsx_n),
2329 y1 + tooth_size2,
2330 ]
2331 left_saw_x = [
2332 x0,
2333 *([x0 - tooth_size2, x0, x0 + tooth_size2, x0] * dsy_n),
2334 x0 - tooth_size2,
2335 ]
2336 left_saw_y = [
2337 y1,
2338 *(y1 - tooth_size2 - dsy * .5 * np.arange(dsy_n * 2)),
2339 y0 + tooth_size2,
2340 ]
2342 saw_vertices = [*zip(bottom_saw_x, bottom_saw_y),
2343 *zip(right_saw_x, right_saw_y),
2344 *zip(top_saw_x, top_saw_y),
2345 *zip(left_saw_x, left_saw_y),
2346 (bottom_saw_x[0], bottom_saw_y[0])]
2348 return saw_vertices
2350 def transmute(self, x0, y0, width, height, mutation_size):
2351 saw_vertices = self._get_sawtooth_vertices(x0, y0, width,
2352 height, mutation_size)
2353 path = Path(saw_vertices, closed=True)
2354 return path
2356 @_register_style(_style_list)
2357 class Roundtooth(Sawtooth):
2358 """
2359 A box with a rounded sawtooth outline.
2361 Parameters
2362 ----------
2363 pad : float, default: 0.3
2364 The amount of padding around the original box.
2365 tooth_size : float, default: *pad*/2
2366 Size of the sawtooth.
2367 """
2368 def __init__(self, pad=0.3, tooth_size=None):
2369 super().__init__(pad, tooth_size)
2371 def transmute(self, x0, y0, width, height, mutation_size):
2372 saw_vertices = self._get_sawtooth_vertices(x0, y0,
2373 width, height,
2374 mutation_size)
2375 # Add a trailing vertex to allow us to close the polygon correctly
2376 saw_vertices = np.concatenate([np.array(saw_vertices),
2377 [saw_vertices[0]]], axis=0)
2378 codes = ([Path.MOVETO] +
2379 [Path.CURVE3, Path.CURVE3] * ((len(saw_vertices)-1)//2) +
2380 [Path.CLOSEPOLY])
2381 return Path(saw_vertices, codes)
2383 if __doc__: # __doc__ could be None if -OO optimization is enabled
2384 __doc__ = inspect.cleandoc(__doc__) % {
2385 "AvailableBoxstyles": _pprint_styles(_style_list)}
2387docstring.interpd.update(
2388 AvailableBoxstyles=_pprint_styles(BoxStyle._style_list),
2389 ListBoxstyles=_simpleprint_styles(BoxStyle._style_list))
2392class FancyBboxPatch(Patch):
2393 """
2394 A fancy box around a rectangle with lower left at *xy* = (*x*, *y*)
2395 with specified width and height.
2397 `.FancyBboxPatch` is similar to `.Rectangle`, but it draws a fancy box
2398 around the rectangle. The transformation of the rectangle box to the
2399 fancy box is delegated to the style classes defined in `.BoxStyle`.
2400 """
2402 _edge_default = True
2404 def __str__(self):
2405 s = self.__class__.__name__ + "((%g, %g), width=%g, height=%g)"
2406 return s % (self._x, self._y, self._width, self._height)
2408 @docstring.dedent_interpd
2409 def __init__(self, xy, width, height,
2410 boxstyle="round",
2411 bbox_transmuter=None,
2412 mutation_scale=1.,
2413 mutation_aspect=None,
2414 **kwargs):
2415 """
2416 Parameters
2417 ----------
2418 xy : float, float
2419 The lower left corner of the box.
2421 width : float
2422 The width of the box.
2424 height : float
2425 The height of the box.
2427 boxstyle : str or `matplotlib.patches.BoxStyle`
2428 The style of the fancy box. This can either be a `.BoxStyle`
2429 instance or a string of the style name and optionally comma
2430 seprarated attributes (e.g. "Round, pad=0.2"). This string is
2431 passed to `.BoxStyle` to construct a `.BoxStyle` object. See
2432 there for a full documentation.
2434 The following box styles are available:
2436 %(AvailableBoxstyles)s
2438 mutation_scale : float, optional, default: 1
2439 Scaling factor applied to the attributes of the box style
2440 (e.g. pad or rounding_size).
2442 mutation_aspect : float, optional
2443 The height of the rectangle will be squeezed by this value before
2444 the mutation and the mutated box will be stretched by the inverse
2445 of it. For example, this allows different horizontal and vertical
2446 padding.
2448 Other Parameters
2449 ----------------
2450 **kwargs : `.Patch` properties
2452 %(Patch)s
2453 """
2455 Patch.__init__(self, **kwargs)
2457 self._x = xy[0]
2458 self._y = xy[1]
2459 self._width = width
2460 self._height = height
2462 if boxstyle == "custom":
2463 if bbox_transmuter is None:
2464 raise ValueError("bbox_transmuter argument is needed with "
2465 "custom boxstyle")
2466 self._bbox_transmuter = bbox_transmuter
2467 else:
2468 self.set_boxstyle(boxstyle)
2470 self._mutation_scale = mutation_scale
2471 self._mutation_aspect = mutation_aspect
2473 self.stale = True
2475 @docstring.dedent_interpd
2476 def set_boxstyle(self, boxstyle=None, **kwargs):
2477 """
2478 Set the box style.
2480 Most box styles can be further configured using attributes.
2481 Attributes from the previous box style are not reused.
2483 Without argument (or with ``boxstyle=None``), the available box styles
2484 are returned as a human-readable string.
2486 Parameters
2487 ----------
2488 boxstyle : str
2489 The name of the box style. Optionally, followed by a comma and a
2490 comma-separated list of attributes. The attributes may
2491 alternatively be passed separately as keyword arguments.
2493 The following box styles are available:
2495 %(AvailableBoxstyles)s
2497 .. ACCEPTS: %(ListBoxstyles)s
2499 **kwargs
2500 Additional attributes for the box style. See the table above for
2501 supported parameters.
2503 Examples
2504 --------
2505 ::
2507 set_boxstyle("round,pad=0.2")
2508 set_boxstyle("round", pad=0.2)
2510 """
2511 if boxstyle is None:
2512 return BoxStyle.pprint_styles()
2514 if isinstance(boxstyle, BoxStyle._Base) or callable(boxstyle):
2515 self._bbox_transmuter = boxstyle
2516 else:
2517 self._bbox_transmuter = BoxStyle(boxstyle, **kwargs)
2518 self.stale = True
2520 def set_mutation_scale(self, scale):
2521 """
2522 Set the mutation scale.
2524 Parameters
2525 ----------
2526 scale : float
2527 """
2528 self._mutation_scale = scale
2529 self.stale = True
2531 def get_mutation_scale(self):
2532 """Return the mutation scale."""
2533 return self._mutation_scale
2535 def set_mutation_aspect(self, aspect):
2536 """
2537 Set the aspect ratio of the bbox mutation.
2539 Parameters
2540 ----------
2541 aspect : float
2542 """
2543 self._mutation_aspect = aspect
2544 self.stale = True
2546 def get_mutation_aspect(self):
2547 """Return the aspect ratio of the bbox mutation."""
2548 return self._mutation_aspect
2550 def get_boxstyle(self):
2551 """Return the boxstyle object."""
2552 return self._bbox_transmuter
2554 def get_path(self):
2555 """Return the mutated path of the rectangle."""
2556 _path = self.get_boxstyle()(self._x, self._y,
2557 self._width, self._height,
2558 self.get_mutation_scale(),
2559 self.get_mutation_aspect())
2560 return _path
2562 # Following methods are borrowed from the Rectangle class.
2564 def get_x(self):
2565 """Return the left coord of the rectangle."""
2566 return self._x
2568 def get_y(self):
2569 """Return the bottom coord of the rectangle."""
2570 return self._y
2572 def get_width(self):
2573 """Return the width of the rectangle."""
2574 return self._width
2576 def get_height(self):
2577 """Return the height of the rectangle."""
2578 return self._height
2580 def set_x(self, x):
2581 """
2582 Set the left coord of the rectangle.
2584 Parameters
2585 ----------
2586 x : float
2587 """
2588 self._x = x
2589 self.stale = True
2591 def set_y(self, y):
2592 """
2593 Set the bottom coord of the rectangle.
2595 Parameters
2596 ----------
2597 y : float
2598 """
2599 self._y = y
2600 self.stale = True
2602 def set_width(self, w):
2603 """
2604 Set the rectangle width.
2606 Parameters
2607 ----------
2608 w : float
2609 """
2610 self._width = w
2611 self.stale = True
2613 def set_height(self, h):
2614 """
2615 Set the rectangle height.
2617 Parameters
2618 ----------
2619 h : float
2620 """
2621 self._height = h
2622 self.stale = True
2624 def set_bounds(self, *args):
2625 """
2626 Set the bounds of the rectangle.
2628 Call signatures::
2630 set_bounds(left, bottom, width, height)
2631 set_bounds((left, bottom, width, height))
2633 Parameters
2634 ----------
2635 left, bottom : float
2636 The coordinates of the bottom left corner of the rectangle.
2637 width, height : float
2638 The width/height of the rectangle.
2639 """
2640 if len(args) == 1:
2641 l, b, w, h = args[0]
2642 else:
2643 l, b, w, h = args
2644 self._x = l
2645 self._y = b
2646 self._width = w
2647 self._height = h
2648 self.stale = True
2650 def get_bbox(self):
2651 """Return the `.Bbox`."""
2652 return transforms.Bbox.from_bounds(self._x, self._y,
2653 self._width, self._height)
2656class ConnectionStyle(_Style):
2657 """
2658 :class:`ConnectionStyle` is a container class which defines
2659 several connectionstyle classes, which is used to create a path
2660 between two points. These are mainly used with
2661 :class:`FancyArrowPatch`.
2663 A connectionstyle object can be either created as::
2665 ConnectionStyle.Arc3(rad=0.2)
2667 or::
2669 ConnectionStyle("Arc3", rad=0.2)
2671 or::
2673 ConnectionStyle("Arc3, rad=0.2")
2675 The following classes are defined
2677 %(AvailableConnectorstyles)s
2679 An instance of any connection style class is an callable object,
2680 whose call signature is::
2682 __call__(self, posA, posB,
2683 patchA=None, patchB=None,
2684 shrinkA=2., shrinkB=2.)
2686 and it returns a :class:`Path` instance. *posA* and *posB* are
2687 tuples of (x, y) coordinates of the two points to be
2688 connected. *patchA* (or *patchB*) is given, the returned path is
2689 clipped so that it start (or end) from the boundary of the
2690 patch. The path is further shrunk by *shrinkA* (or *shrinkB*)
2691 which is given in points.
2692 """
2694 _style_list = {}
2696 class _Base:
2697 """
2698 A base class for connectionstyle classes. The subclass needs
2699 to implement a *connect* method whose call signature is::
2701 connect(posA, posB)
2703 where posA and posB are tuples of x, y coordinates to be
2704 connected. The method needs to return a path connecting two
2705 points. This base class defines a __call__ method, and a few
2706 helper methods.
2707 """
2709 class SimpleEvent:
2710 def __init__(self, xy):
2711 self.x, self.y = xy
2713 def _clip(self, path, patchA, patchB):
2714 """
2715 Clip the path to the boundary of the patchA and patchB.
2716 The starting point of the path needed to be inside of the
2717 patchA and the end point inside the patch B. The *contains*
2718 methods of each patch object is utilized to test if the point
2719 is inside the path.
2720 """
2722 if patchA:
2723 def insideA(xy_display):
2724 xy_event = ConnectionStyle._Base.SimpleEvent(xy_display)
2725 return patchA.contains(xy_event)[0]
2727 try:
2728 left, right = split_path_inout(path, insideA)
2729 except ValueError:
2730 right = path
2732 path = right
2734 if patchB:
2735 def insideB(xy_display):
2736 xy_event = ConnectionStyle._Base.SimpleEvent(xy_display)
2737 return patchB.contains(xy_event)[0]
2739 try:
2740 left, right = split_path_inout(path, insideB)
2741 except ValueError:
2742 left = path
2744 path = left
2746 return path
2748 def _shrink(self, path, shrinkA, shrinkB):
2749 """
2750 Shrink the path by fixed size (in points) with shrinkA and shrinkB.
2751 """
2752 if shrinkA:
2753 insideA = inside_circle(*path.vertices[0], shrinkA)
2754 try:
2755 left, path = split_path_inout(path, insideA)
2756 except ValueError:
2757 pass
2758 if shrinkB:
2759 insideB = inside_circle(*path.vertices[-1], shrinkB)
2760 try:
2761 path, right = split_path_inout(path, insideB)
2762 except ValueError:
2763 pass
2764 return path
2766 def __call__(self, posA, posB,
2767 shrinkA=2., shrinkB=2., patchA=None, patchB=None):
2768 """
2769 Calls the *connect* method to create a path between *posA*
2770 and *posB*. The path is clipped and shrunken.
2771 """
2773 path = self.connect(posA, posB)
2775 clipped_path = self._clip(path, patchA, patchB)
2776 shrunk_path = self._shrink(clipped_path, shrinkA, shrinkB)
2778 return shrunk_path
2780 @_register_style(_style_list)
2781 class Arc3(_Base):
2782 """
2783 Creates a simple quadratic Bezier curve between two
2784 points. The curve is created so that the middle control point
2785 (C1) is located at the same distance from the start (C0) and
2786 end points(C2) and the distance of the C1 to the line
2787 connecting C0-C2 is *rad* times the distance of C0-C2.
2788 """
2790 def __init__(self, rad=0.):
2791 """
2792 *rad*
2793 curvature of the curve.
2794 """
2795 self.rad = rad
2797 def connect(self, posA, posB):
2798 x1, y1 = posA
2799 x2, y2 = posB
2800 x12, y12 = (x1 + x2) / 2., (y1 + y2) / 2.
2801 dx, dy = x2 - x1, y2 - y1
2803 f = self.rad
2805 cx, cy = x12 + f * dy, y12 - f * dx
2807 vertices = [(x1, y1),
2808 (cx, cy),
2809 (x2, y2)]
2810 codes = [Path.MOVETO,
2811 Path.CURVE3,
2812 Path.CURVE3]
2814 return Path(vertices, codes)
2816 @_register_style(_style_list)
2817 class Angle3(_Base):
2818 """
2819 Creates a simple quadratic Bezier curve between two
2820 points. The middle control points is placed at the
2821 intersecting point of two lines which cross the start and
2822 end point, and have a slope of angleA and angleB, respectively.
2823 """
2825 def __init__(self, angleA=90, angleB=0):
2826 """
2827 *angleA*
2828 starting angle of the path
2830 *angleB*
2831 ending angle of the path
2832 """
2834 self.angleA = angleA
2835 self.angleB = angleB
2837 def connect(self, posA, posB):
2838 x1, y1 = posA
2839 x2, y2 = posB
2841 cosA = math.cos(math.radians(self.angleA))
2842 sinA = math.sin(math.radians(self.angleA))
2843 cosB = math.cos(math.radians(self.angleB))
2844 sinB = math.sin(math.radians(self.angleB))
2846 cx, cy = get_intersection(x1, y1, cosA, sinA,
2847 x2, y2, cosB, sinB)
2849 vertices = [(x1, y1), (cx, cy), (x2, y2)]
2850 codes = [Path.MOVETO, Path.CURVE3, Path.CURVE3]
2852 return Path(vertices, codes)
2854 @_register_style(_style_list)
2855 class Angle(_Base):
2856 """
2857 Creates a piecewise continuous quadratic Bezier path between
2858 two points. The path has a one passing-through point placed at
2859 the intersecting point of two lines which cross the start
2860 and end point, and have a slope of angleA and angleB, respectively.
2861 The connecting edges are rounded with *rad*.
2862 """
2864 def __init__(self, angleA=90, angleB=0, rad=0.):
2865 """
2866 *angleA*
2867 starting angle of the path
2869 *angleB*
2870 ending angle of the path
2872 *rad*
2873 rounding radius of the edge
2874 """
2876 self.angleA = angleA
2877 self.angleB = angleB
2879 self.rad = rad
2881 def connect(self, posA, posB):
2882 x1, y1 = posA
2883 x2, y2 = posB
2885 cosA = math.cos(math.radians(self.angleA))
2886 sinA = math.sin(math.radians(self.angleA))
2887 cosB = math.cos(math.radians(self.angleB))
2888 sinB = math.sin(math.radians(self.angleB))
2890 cx, cy = get_intersection(x1, y1, cosA, sinA,
2891 x2, y2, cosB, sinB)
2893 vertices = [(x1, y1)]
2894 codes = [Path.MOVETO]
2896 if self.rad == 0.:
2897 vertices.append((cx, cy))
2898 codes.append(Path.LINETO)
2899 else:
2900 dx1, dy1 = x1 - cx, y1 - cy
2901 d1 = np.hypot(dx1, dy1)
2902 f1 = self.rad / d1
2903 dx2, dy2 = x2 - cx, y2 - cy
2904 d2 = np.hypot(dx2, dy2)
2905 f2 = self.rad / d2
2906 vertices.extend([(cx + dx1 * f1, cy + dy1 * f1),
2907 (cx, cy),
2908 (cx + dx2 * f2, cy + dy2 * f2)])
2909 codes.extend([Path.LINETO, Path.CURVE3, Path.CURVE3])
2911 vertices.append((x2, y2))
2912 codes.append(Path.LINETO)
2914 return Path(vertices, codes)
2916 @_register_style(_style_list)
2917 class Arc(_Base):
2918 """
2919 Creates a piecewise continuous quadratic Bezier path between
2920 two points. The path can have two passing-through points, a
2921 point placed at the distance of armA and angle of angleA from
2922 point A, another point with respect to point B. The edges are
2923 rounded with *rad*.
2924 """
2926 def __init__(self, angleA=0, angleB=0, armA=None, armB=None, rad=0.):
2927 """
2928 *angleA* :
2929 starting angle of the path
2931 *angleB* :
2932 ending angle of the path
2934 *armA* :
2935 length of the starting arm
2937 *armB* :
2938 length of the ending arm
2940 *rad* :
2941 rounding radius of the edges
2942 """
2944 self.angleA = angleA
2945 self.angleB = angleB
2946 self.armA = armA
2947 self.armB = armB
2949 self.rad = rad
2951 def connect(self, posA, posB):
2952 x1, y1 = posA
2953 x2, y2 = posB
2955 vertices = [(x1, y1)]
2956 rounded = []
2957 codes = [Path.MOVETO]
2959 if self.armA:
2960 cosA = math.cos(math.radians(self.angleA))
2961 sinA = math.sin(math.radians(self.angleA))
2962 # x_armA, y_armB
2963 d = self.armA - self.rad
2964 rounded.append((x1 + d * cosA, y1 + d * sinA))
2965 d = self.armA
2966 rounded.append((x1 + d * cosA, y1 + d * sinA))
2968 if self.armB:
2969 cosB = math.cos(math.radians(self.angleB))
2970 sinB = math.sin(math.radians(self.angleB))
2971 x_armB, y_armB = x2 + self.armB * cosB, y2 + self.armB * sinB
2973 if rounded:
2974 xp, yp = rounded[-1]
2975 dx, dy = x_armB - xp, y_armB - yp
2976 dd = (dx * dx + dy * dy) ** .5
2978 rounded.append((xp + self.rad * dx / dd,
2979 yp + self.rad * dy / dd))
2980 vertices.extend(rounded)
2981 codes.extend([Path.LINETO,
2982 Path.CURVE3,
2983 Path.CURVE3])
2984 else:
2985 xp, yp = vertices[-1]
2986 dx, dy = x_armB - xp, y_armB - yp
2987 dd = (dx * dx + dy * dy) ** .5
2989 d = dd - self.rad
2990 rounded = [(xp + d * dx / dd, yp + d * dy / dd),
2991 (x_armB, y_armB)]
2993 if rounded:
2994 xp, yp = rounded[-1]
2995 dx, dy = x2 - xp, y2 - yp
2996 dd = (dx * dx + dy * dy) ** .5
2998 rounded.append((xp + self.rad * dx / dd,
2999 yp + self.rad * dy / dd))
3000 vertices.extend(rounded)
3001 codes.extend([Path.LINETO,
3002 Path.CURVE3,
3003 Path.CURVE3])
3005 vertices.append((x2, y2))
3006 codes.append(Path.LINETO)
3008 return Path(vertices, codes)
3010 @_register_style(_style_list)
3011 class Bar(_Base):
3012 """
3013 A line with *angle* between A and B with *armA* and
3014 *armB*. One of the arms is extended so that they are connected in
3015 a right angle. The length of armA is determined by (*armA*
3016 + *fraction* x AB distance). Same for armB.
3017 """
3019 def __init__(self, armA=0., armB=0., fraction=0.3, angle=None):
3020 """
3021 Parameters
3022 ----------
3023 armA : float
3024 minimum length of armA
3026 armB : float
3027 minimum length of armB
3029 fraction : float
3030 a fraction of the distance between two points that
3031 will be added to armA and armB.
3033 angle : float or None
3034 angle of the connecting line (if None, parallel
3035 to A and B)
3036 """
3037 self.armA = armA
3038 self.armB = armB
3039 self.fraction = fraction
3040 self.angle = angle
3042 def connect(self, posA, posB):
3043 x1, y1 = posA
3044 x20, y20 = x2, y2 = posB
3046 theta1 = math.atan2(y2 - y1, x2 - x1)
3047 dx, dy = x2 - x1, y2 - y1
3048 dd = (dx * dx + dy * dy) ** .5
3049 ddx, ddy = dx / dd, dy / dd
3051 armA, armB = self.armA, self.armB
3053 if self.angle is not None:
3054 theta0 = np.deg2rad(self.angle)
3055 dtheta = theta1 - theta0
3056 dl = dd * math.sin(dtheta)
3057 dL = dd * math.cos(dtheta)
3058 x2, y2 = x1 + dL * math.cos(theta0), y1 + dL * math.sin(theta0)
3059 armB = armB - dl
3061 # update
3062 dx, dy = x2 - x1, y2 - y1
3063 dd2 = (dx * dx + dy * dy) ** .5
3064 ddx, ddy = dx / dd2, dy / dd2
3066 arm = max(armA, armB)
3067 f = self.fraction * dd + arm
3069 cx1, cy1 = x1 + f * ddy, y1 - f * ddx
3070 cx2, cy2 = x2 + f * ddy, y2 - f * ddx
3072 vertices = [(x1, y1),
3073 (cx1, cy1),
3074 (cx2, cy2),
3075 (x20, y20)]
3076 codes = [Path.MOVETO,
3077 Path.LINETO,
3078 Path.LINETO,
3079 Path.LINETO]
3081 return Path(vertices, codes)
3083 if __doc__:
3084 __doc__ = inspect.cleandoc(__doc__) % {
3085 "AvailableConnectorstyles": _pprint_styles(_style_list)}
3088def _point_along_a_line(x0, y0, x1, y1, d):
3089 """
3090 Return the point on the line connecting (*x0*, *y0*) -- (*x1*, *y1*) whose
3091 distance from (*x0*, *y0*) is *d*.
3092 """
3093 dx, dy = x0 - x1, y0 - y1
3094 ff = d / (dx * dx + dy * dy) ** .5
3095 x2, y2 = x0 - ff * dx, y0 - ff * dy
3097 return x2, y2
3100class ArrowStyle(_Style):
3101 """
3102 :class:`ArrowStyle` is a container class which defines several
3103 arrowstyle classes, which is used to create an arrow path along a
3104 given path. These are mainly used with :class:`FancyArrowPatch`.
3106 A arrowstyle object can be either created as::
3108 ArrowStyle.Fancy(head_length=.4, head_width=.4, tail_width=.4)
3110 or::
3112 ArrowStyle("Fancy", head_length=.4, head_width=.4, tail_width=.4)
3114 or::
3116 ArrowStyle("Fancy, head_length=.4, head_width=.4, tail_width=.4")
3118 The following classes are defined
3120 %(AvailableArrowstyles)s
3122 An instance of any arrow style class is a callable object,
3123 whose call signature is::
3125 __call__(self, path, mutation_size, linewidth, aspect_ratio=1.)
3127 and it returns a tuple of a :class:`Path` instance and a boolean
3128 value. *path* is a :class:`Path` instance along which the arrow
3129 will be drawn. *mutation_size* and *aspect_ratio* have the same
3130 meaning as in :class:`BoxStyle`. *linewidth* is a line width to be
3131 stroked. This is meant to be used to correct the location of the
3132 head so that it does not overshoot the destination point, but not all
3133 classes support it.
3134 """
3136 _style_list = {}
3138 class _Base:
3139 """
3140 Arrow Transmuter Base class
3142 ArrowTransmuterBase and its derivatives are used to make a fancy
3143 arrow around a given path. The __call__ method returns a path
3144 (which will be used to create a PathPatch instance) and a boolean
3145 value indicating the path is open therefore is not fillable. This
3146 class is not an artist and actual drawing of the fancy arrow is
3147 done by the FancyArrowPatch class.
3149 """
3151 # The derived classes are required to be able to be initialized
3152 # w/o arguments, i.e., all its argument (except self) must have
3153 # the default values.
3155 @staticmethod
3156 def ensure_quadratic_bezier(path):
3157 """
3158 Some ArrowStyle class only works with a simple quadratic Bezier
3159 curve (created with Arc3Connection or Angle3Connector). This static
3160 method is to check if the provided path is a simple quadratic
3161 Bezier curve and returns its control points if true.
3162 """
3163 segments = list(path.iter_segments())
3164 if (len(segments) != 2 or segments[0][1] != Path.MOVETO or
3165 segments[1][1] != Path.CURVE3):
3166 raise ValueError(
3167 "'path' is not a valid quadratic Bezier curve")
3168 return [*segments[0][0], *segments[1][0]]
3170 def transmute(self, path, mutation_size, linewidth):
3171 """
3172 The transmute method is the very core of the ArrowStyle class and
3173 must be overridden in the subclasses. It receives the path object
3174 along which the arrow will be drawn, and the mutation_size, with
3175 which the arrow head etc. will be scaled. The linewidth may be
3176 used to adjust the path so that it does not pass beyond the given
3177 points. It returns a tuple of a Path instance and a boolean. The
3178 boolean value indicate whether the path can be filled or not. The
3179 return value can also be a list of paths and list of booleans of a
3180 same length.
3181 """
3182 raise NotImplementedError('Derived must override')
3184 def __call__(self, path, mutation_size, linewidth,
3185 aspect_ratio=1.):
3186 """
3187 The __call__ method is a thin wrapper around the transmute method
3188 and takes care of the aspect ratio.
3189 """
3191 path = make_path_regular(path)
3193 if aspect_ratio is not None:
3194 # Squeeze the given height by the aspect_ratio
3196 vertices, codes = path.vertices[:], path.codes[:]
3197 # Squeeze the height
3198 vertices[:, 1] = vertices[:, 1] / aspect_ratio
3199 path_shrunk = Path(vertices, codes)
3200 # call transmute method with squeezed height.
3201 path_mutated, fillable = self.transmute(path_shrunk,
3202 linewidth,
3203 mutation_size)
3204 if np.iterable(fillable):
3205 path_list = []
3206 for p in zip(path_mutated):
3207 v, c = p.vertices, p.codes
3208 # Restore the height
3209 v[:, 1] = v[:, 1] * aspect_ratio
3210 path_list.append(Path(v, c))
3211 return path_list, fillable
3212 else:
3213 return path_mutated, fillable
3214 else:
3215 return self.transmute(path, mutation_size, linewidth)
3217 class _Curve(_Base):
3218 """
3219 A simple arrow which will work with any path instance. The
3220 returned path is simply concatenation of the original path + at
3221 most two paths representing the arrow head at the begin point and the
3222 at the end point. The arrow heads can be either open or closed.
3223 """
3225 def __init__(self, beginarrow=None, endarrow=None,
3226 fillbegin=False, fillend=False,
3227 head_length=.2, head_width=.1):
3228 """
3229 The arrows are drawn if *beginarrow* and/or *endarrow* are
3230 true. *head_length* and *head_width* determines the size
3231 of the arrow relative to the *mutation scale*. The
3232 arrowhead at the begin (or end) is closed if fillbegin (or
3233 fillend) is True.
3234 """
3235 self.beginarrow, self.endarrow = beginarrow, endarrow
3236 self.head_length, self.head_width = head_length, head_width
3237 self.fillbegin, self.fillend = fillbegin, fillend
3238 super().__init__()
3240 def _get_arrow_wedge(self, x0, y0, x1, y1,
3241 head_dist, cos_t, sin_t, linewidth):
3242 """
3243 Return the paths for arrow heads. Since arrow lines are
3244 drawn with capstyle=projected, The arrow goes beyond the
3245 desired point. This method also returns the amount of the path
3246 to be shrunken so that it does not overshoot.
3247 """
3249 # arrow from x0, y0 to x1, y1
3250 dx, dy = x0 - x1, y0 - y1
3252 cp_distance = np.hypot(dx, dy)
3254 # pad_projected : amount of pad to account the
3255 # overshooting of the projection of the wedge
3256 pad_projected = (.5 * linewidth / sin_t)
3258 # Account for division by zero
3259 if cp_distance == 0:
3260 cp_distance = 1
3262 # apply pad for projected edge
3263 ddx = pad_projected * dx / cp_distance
3264 ddy = pad_projected * dy / cp_distance
3266 # offset for arrow wedge
3267 dx = dx / cp_distance * head_dist
3268 dy = dy / cp_distance * head_dist
3270 dx1, dy1 = cos_t * dx + sin_t * dy, -sin_t * dx + cos_t * dy
3271 dx2, dy2 = cos_t * dx - sin_t * dy, sin_t * dx + cos_t * dy
3273 vertices_arrow = [(x1 + ddx + dx1, y1 + ddy + dy1),
3274 (x1 + ddx, y1 + ddy),
3275 (x1 + ddx + dx2, y1 + ddy + dy2)]
3276 codes_arrow = [Path.MOVETO,
3277 Path.LINETO,
3278 Path.LINETO]
3280 return vertices_arrow, codes_arrow, ddx, ddy
3282 def transmute(self, path, mutation_size, linewidth):
3284 head_length = self.head_length * mutation_size
3285 head_width = self.head_width * mutation_size
3286 head_dist = np.hypot(head_length, head_width)
3287 cos_t, sin_t = head_length / head_dist, head_width / head_dist
3289 # begin arrow
3290 x0, y0 = path.vertices[0]
3291 x1, y1 = path.vertices[1]
3293 # If there is no room for an arrow and a line, then skip the arrow
3294 has_begin_arrow = self.beginarrow and (x0, y0) != (x1, y1)
3295 verticesA, codesA, ddxA, ddyA = (
3296 self._get_arrow_wedge(x1, y1, x0, y0,
3297 head_dist, cos_t, sin_t, linewidth)
3298 if has_begin_arrow
3299 else ([], [], 0, 0)
3300 )
3302 # end arrow
3303 x2, y2 = path.vertices[-2]
3304 x3, y3 = path.vertices[-1]
3306 # If there is no room for an arrow and a line, then skip the arrow
3307 has_end_arrow = self.endarrow and (x2, y2) != (x3, y3)
3308 verticesB, codesB, ddxB, ddyB = (
3309 self._get_arrow_wedge(x2, y2, x3, y3,
3310 head_dist, cos_t, sin_t, linewidth)
3311 if has_end_arrow
3312 else ([], [], 0, 0)
3313 )
3315 # This simple code will not work if ddx, ddy is greater than the
3316 # separation between vertices.
3317 _path = [Path(np.concatenate([[(x0 + ddxA, y0 + ddyA)],
3318 path.vertices[1:-1],
3319 [(x3 + ddxB, y3 + ddyB)]]),
3320 path.codes)]
3321 _fillable = [False]
3323 if has_begin_arrow:
3324 if self.fillbegin:
3325 p = np.concatenate([verticesA, [verticesA[0],
3326 verticesA[0]], ])
3327 c = np.concatenate([codesA, [Path.LINETO, Path.CLOSEPOLY]])
3328 _path.append(Path(p, c))
3329 _fillable.append(True)
3330 else:
3331 _path.append(Path(verticesA, codesA))
3332 _fillable.append(False)
3334 if has_end_arrow:
3335 if self.fillend:
3336 _fillable.append(True)
3337 p = np.concatenate([verticesB, [verticesB[0],
3338 verticesB[0]], ])
3339 c = np.concatenate([codesB, [Path.LINETO, Path.CLOSEPOLY]])
3340 _path.append(Path(p, c))
3341 else:
3342 _fillable.append(False)
3343 _path.append(Path(verticesB, codesB))
3345 return _path, _fillable
3347 @_register_style(_style_list, name="-")
3348 class Curve(_Curve):
3349 """
3350 A simple curve without any arrow head.
3351 """
3353 def __init__(self):
3354 super().__init__(beginarrow=False, endarrow=False)
3356 @_register_style(_style_list, name="<-")
3357 class CurveA(_Curve):
3358 """
3359 An arrow with a head at its begin point.
3360 """
3362 def __init__(self, head_length=.4, head_width=.2):
3363 """
3364 Parameters
3365 ----------
3366 head_length : float, optional, default : 0.4
3367 Length of the arrow head
3369 head_width : float, optional, default : 0.2
3370 Width of the arrow head
3371 """
3372 super().__init__(beginarrow=True, endarrow=False,
3373 head_length=head_length, head_width=head_width)
3375 @_register_style(_style_list, name="->")
3376 class CurveB(_Curve):
3377 """
3378 An arrow with a head at its end point.
3379 """
3381 def __init__(self, head_length=.4, head_width=.2):
3382 """
3383 Parameters
3384 ----------
3385 head_length : float, optional, default : 0.4
3386 Length of the arrow head
3388 head_width : float, optional, default : 0.2
3389 Width of the arrow head
3390 """
3391 super().__init__(beginarrow=False, endarrow=True,
3392 head_length=head_length, head_width=head_width)
3394 @_register_style(_style_list, name="<->")
3395 class CurveAB(_Curve):
3396 """
3397 An arrow with heads both at the begin and the end point.
3398 """
3400 def __init__(self, head_length=.4, head_width=.2):
3401 """
3402 Parameters
3403 ----------
3404 head_length : float, optional, default : 0.4
3405 Length of the arrow head
3407 head_width : float, optional, default : 0.2
3408 Width of the arrow head
3409 """
3410 super().__init__(beginarrow=True, endarrow=True,
3411 head_length=head_length, head_width=head_width)
3413 @_register_style(_style_list, name="<|-")
3414 class CurveFilledA(_Curve):
3415 """
3416 An arrow with filled triangle head at the begin.
3417 """
3419 def __init__(self, head_length=.4, head_width=.2):
3420 """
3421 Parameters
3422 ----------
3423 head_length : float, optional, default : 0.4
3424 Length of the arrow head
3426 head_width : float, optional, default : 0.2
3427 Width of the arrow head
3428 """
3429 super().__init__(beginarrow=True, endarrow=False,
3430 fillbegin=True, fillend=False,
3431 head_length=head_length, head_width=head_width)
3433 @_register_style(_style_list, name="-|>")
3434 class CurveFilledB(_Curve):
3435 """
3436 An arrow with filled triangle head at the end.
3437 """
3439 def __init__(self, head_length=.4, head_width=.2):
3440 """
3441 Parameters
3442 ----------
3443 head_length : float, optional, default : 0.4
3444 Length of the arrow head
3446 head_width : float, optional, default : 0.2
3447 Width of the arrow head
3448 """
3449 super().__init__(beginarrow=False, endarrow=True,
3450 fillbegin=False, fillend=True,
3451 head_length=head_length, head_width=head_width)
3453 @_register_style(_style_list, name="<|-|>")
3454 class CurveFilledAB(_Curve):
3455 """
3456 An arrow with filled triangle heads at both ends.
3457 """
3459 def __init__(self, head_length=.4, head_width=.2):
3460 """
3461 Parameters
3462 ----------
3463 head_length : float, optional, default : 0.4
3464 Length of the arrow head
3466 head_width : float, optional, default : 0.2
3467 Width of the arrow head
3468 """
3469 super().__init__(beginarrow=True, endarrow=True,
3470 fillbegin=True, fillend=True,
3471 head_length=head_length, head_width=head_width)
3473 class _Bracket(_Base):
3475 def __init__(self, bracketA=None, bracketB=None,
3476 widthA=1., widthB=1.,
3477 lengthA=0.2, lengthB=0.2,
3478 angleA=None, angleB=None,
3479 scaleA=None, scaleB=None):
3480 self.bracketA, self.bracketB = bracketA, bracketB
3481 self.widthA, self.widthB = widthA, widthB
3482 self.lengthA, self.lengthB = lengthA, lengthB
3483 self.angleA, self.angleB = angleA, angleB
3484 self.scaleA, self.scaleB = scaleA, scaleB
3486 def _get_bracket(self, x0, y0,
3487 cos_t, sin_t, width, length):
3489 # arrow from x0, y0 to x1, y1
3490 from matplotlib.bezier import get_normal_points
3491 x1, y1, x2, y2 = get_normal_points(x0, y0, cos_t, sin_t, width)
3493 dx, dy = length * cos_t, length * sin_t
3495 vertices_arrow = [(x1 + dx, y1 + dy),
3496 (x1, y1),
3497 (x2, y2),
3498 (x2 + dx, y2 + dy)]
3499 codes_arrow = [Path.MOVETO,
3500 Path.LINETO,
3501 Path.LINETO,
3502 Path.LINETO]
3504 return vertices_arrow, codes_arrow
3506 def transmute(self, path, mutation_size, linewidth):
3508 if self.scaleA is None:
3509 scaleA = mutation_size
3510 else:
3511 scaleA = self.scaleA
3513 if self.scaleB is None:
3514 scaleB = mutation_size
3515 else:
3516 scaleB = self.scaleB
3518 vertices_list, codes_list = [], []
3520 if self.bracketA:
3521 x0, y0 = path.vertices[0]
3522 x1, y1 = path.vertices[1]
3523 cos_t, sin_t = get_cos_sin(x1, y1, x0, y0)
3524 verticesA, codesA = self._get_bracket(x0, y0, cos_t, sin_t,
3525 self.widthA * scaleA,
3526 self.lengthA * scaleA)
3527 vertices_list.append(verticesA)
3528 codes_list.append(codesA)
3530 vertices_list.append(path.vertices)
3531 codes_list.append(path.codes)
3533 if self.bracketB:
3534 x0, y0 = path.vertices[-1]
3535 x1, y1 = path.vertices[-2]
3536 cos_t, sin_t = get_cos_sin(x1, y1, x0, y0)
3537 verticesB, codesB = self._get_bracket(x0, y0, cos_t, sin_t,
3538 self.widthB * scaleB,
3539 self.lengthB * scaleB)
3540 vertices_list.append(verticesB)
3541 codes_list.append(codesB)
3543 vertices = np.concatenate(vertices_list)
3544 codes = np.concatenate(codes_list)
3546 p = Path(vertices, codes)
3548 return p, False
3550 @_register_style(_style_list, name="]-[")
3551 class BracketAB(_Bracket):
3552 """
3553 An arrow with a bracket(]) at both ends.
3554 """
3556 def __init__(self,
3557 widthA=1., lengthA=0.2, angleA=None,
3558 widthB=1., lengthB=0.2, angleB=None):
3559 """
3560 Parameters
3561 ----------
3562 widthA : float, optional, default : 1.0
3563 Width of the bracket
3565 lengthA : float, optional, default : 0.2
3566 Length of the bracket
3568 angleA : float, optional, default : None
3569 Angle between the bracket and the line
3571 widthB : float, optional, default : 1.0
3572 Width of the bracket
3574 lengthB : float, optional, default : 0.2
3575 Length of the bracket
3577 angleB : float, optional, default : None
3578 Angle between the bracket and the line
3579 """
3580 super().__init__(True, True,
3581 widthA=widthA, lengthA=lengthA, angleA=angleA,
3582 widthB=widthB, lengthB=lengthB, angleB=angleB)
3584 @_register_style(_style_list, name="]-")
3585 class BracketA(_Bracket):
3586 """
3587 An arrow with a bracket(]) at its end.
3588 """
3590 def __init__(self, widthA=1., lengthA=0.2, angleA=None):
3591 """
3592 Parameters
3593 ----------
3594 widthA : float, optional, default : 1.0
3595 Width of the bracket
3597 lengthA : float, optional, default : 0.2
3598 Length of the bracket
3600 angleA : float, optional, default : None
3601 Angle between the bracket and the line
3602 """
3603 super().__init__(True, None,
3604 widthA=widthA, lengthA=lengthA, angleA=angleA)
3606 @_register_style(_style_list, name="-[")
3607 class BracketB(_Bracket):
3608 """
3609 An arrow with a bracket([) at its end.
3610 """
3612 def __init__(self, widthB=1., lengthB=0.2, angleB=None):
3613 """
3614 Parameters
3615 ----------
3616 widthB : float, optional, default : 1.0
3617 Width of the bracket
3619 lengthB : float, optional, default : 0.2
3620 Length of the bracket
3622 angleB : float, optional, default : None
3623 Angle between the bracket and the line
3624 """
3625 super().__init__(None, True,
3626 widthB=widthB, lengthB=lengthB, angleB=angleB)
3628 @_register_style(_style_list, name="|-|")
3629 class BarAB(_Bracket):
3630 """
3631 An arrow with a bar(|) at both ends.
3632 """
3634 def __init__(self,
3635 widthA=1., angleA=None,
3636 widthB=1., angleB=None):
3637 """
3638 Parameters
3639 ----------
3640 widthA : float, optional, default : 1.0
3641 Width of the bracket
3643 angleA : float, optional, default : None
3644 Angle between the bracket and the line
3646 widthB : float, optional, default : 1.0
3647 Width of the bracket
3649 angleB : float, optional, default : None
3650 Angle between the bracket and the line
3651 """
3652 super().__init__(True, True,
3653 widthA=widthA, lengthA=0, angleA=angleA,
3654 widthB=widthB, lengthB=0, angleB=angleB)
3656 @_register_style(_style_list)
3657 class Simple(_Base):
3658 """
3659 A simple arrow. Only works with a quadratic Bezier curve.
3660 """
3662 def __init__(self, head_length=.5, head_width=.5, tail_width=.2):
3663 """
3664 Parameters
3665 ----------
3666 head_length : float, optional, default : 0.5
3667 Length of the arrow head
3669 head_width : float, optional, default : 0.5
3670 Width of the arrow head
3672 tail_width : float, optional, default : 0.2
3673 Width of the arrow tail
3674 """
3675 self.head_length, self.head_width, self.tail_width = \
3676 head_length, head_width, tail_width
3677 super().__init__()
3679 def transmute(self, path, mutation_size, linewidth):
3681 x0, y0, x1, y1, x2, y2 = self.ensure_quadratic_bezier(path)
3683 # divide the path into a head and a tail
3684 head_length = self.head_length * mutation_size
3685 in_f = inside_circle(x2, y2, head_length)
3686 arrow_path = [(x0, y0), (x1, y1), (x2, y2)]
3688 try:
3689 arrow_out, arrow_in = \
3690 split_bezier_intersecting_with_closedpath(
3691 arrow_path, in_f, tolerance=0.01)
3692 except NonIntersectingPathException:
3693 # if this happens, make a straight line of the head_length
3694 # long.
3695 x0, y0 = _point_along_a_line(x2, y2, x1, y1, head_length)
3696 x1n, y1n = 0.5 * (x0 + x2), 0.5 * (y0 + y2)
3697 arrow_in = [(x0, y0), (x1n, y1n), (x2, y2)]
3698 arrow_out = None
3700 # head
3701 head_width = self.head_width * mutation_size
3702 head_left, head_right = make_wedged_bezier2(arrow_in,
3703 head_width / 2., wm=.5)
3705 # tail
3706 if arrow_out is not None:
3707 tail_width = self.tail_width * mutation_size
3708 tail_left, tail_right = get_parallels(arrow_out,
3709 tail_width / 2.)
3711 patch_path = [(Path.MOVETO, tail_right[0]),
3712 (Path.CURVE3, tail_right[1]),
3713 (Path.CURVE3, tail_right[2]),
3714 (Path.LINETO, head_right[0]),
3715 (Path.CURVE3, head_right[1]),
3716 (Path.CURVE3, head_right[2]),
3717 (Path.CURVE3, head_left[1]),
3718 (Path.CURVE3, head_left[0]),
3719 (Path.LINETO, tail_left[2]),
3720 (Path.CURVE3, tail_left[1]),
3721 (Path.CURVE3, tail_left[0]),
3722 (Path.LINETO, tail_right[0]),
3723 (Path.CLOSEPOLY, tail_right[0]),
3724 ]
3725 else:
3726 patch_path = [(Path.MOVETO, head_right[0]),
3727 (Path.CURVE3, head_right[1]),
3728 (Path.CURVE3, head_right[2]),
3729 (Path.CURVE3, head_left[1]),
3730 (Path.CURVE3, head_left[0]),
3731 (Path.CLOSEPOLY, head_left[0]),
3732 ]
3734 path = Path([p for c, p in patch_path], [c for c, p in patch_path])
3736 return path, True
3738 @_register_style(_style_list)
3739 class Fancy(_Base):
3740 """
3741 A fancy arrow. Only works with a quadratic Bezier curve.
3742 """
3744 def __init__(self, head_length=.4, head_width=.4, tail_width=.4):
3745 """
3746 Parameters
3747 ----------
3748 head_length : float, optional, default : 0.4
3749 Length of the arrow head
3751 head_width : float, optional, default : 0.4
3752 Width of the arrow head
3754 tail_width : float, optional, default : 0.4
3755 Width of the arrow tail
3756 """
3757 self.head_length, self.head_width, self.tail_width = \
3758 head_length, head_width, tail_width
3759 super().__init__()
3761 def transmute(self, path, mutation_size, linewidth):
3763 x0, y0, x1, y1, x2, y2 = self.ensure_quadratic_bezier(path)
3765 # divide the path into a head and a tail
3766 head_length = self.head_length * mutation_size
3767 arrow_path = [(x0, y0), (x1, y1), (x2, y2)]
3769 # path for head
3770 in_f = inside_circle(x2, y2, head_length)
3771 try:
3772 path_out, path_in = split_bezier_intersecting_with_closedpath(
3773 arrow_path, in_f, tolerance=0.01)
3774 except NonIntersectingPathException:
3775 # if this happens, make a straight line of the head_length
3776 # long.
3777 x0, y0 = _point_along_a_line(x2, y2, x1, y1, head_length)
3778 x1n, y1n = 0.5 * (x0 + x2), 0.5 * (y0 + y2)
3779 arrow_path = [(x0, y0), (x1n, y1n), (x2, y2)]
3780 path_head = arrow_path
3781 else:
3782 path_head = path_in
3784 # path for head
3785 in_f = inside_circle(x2, y2, head_length * .8)
3786 path_out, path_in = split_bezier_intersecting_with_closedpath(
3787 arrow_path, in_f, tolerance=0.01)
3788 path_tail = path_out
3790 # head
3791 head_width = self.head_width * mutation_size
3792 head_l, head_r = make_wedged_bezier2(path_head,
3793 head_width / 2.,
3794 wm=.6)
3796 # tail
3797 tail_width = self.tail_width * mutation_size
3798 tail_left, tail_right = make_wedged_bezier2(path_tail,
3799 tail_width * .5,
3800 w1=1., wm=0.6, w2=0.3)
3802 # path for head
3803 in_f = inside_circle(x0, y0, tail_width * .3)
3804 path_in, path_out = split_bezier_intersecting_with_closedpath(
3805 arrow_path, in_f, tolerance=0.01)
3806 tail_start = path_in[-1]
3808 head_right, head_left = head_r, head_l
3809 patch_path = [(Path.MOVETO, tail_start),
3810 (Path.LINETO, tail_right[0]),
3811 (Path.CURVE3, tail_right[1]),
3812 (Path.CURVE3, tail_right[2]),
3813 (Path.LINETO, head_right[0]),
3814 (Path.CURVE3, head_right[1]),
3815 (Path.CURVE3, head_right[2]),
3816 (Path.CURVE3, head_left[1]),
3817 (Path.CURVE3, head_left[0]),
3818 (Path.LINETO, tail_left[2]),
3819 (Path.CURVE3, tail_left[1]),
3820 (Path.CURVE3, tail_left[0]),
3821 (Path.LINETO, tail_start),
3822 (Path.CLOSEPOLY, tail_start),
3823 ]
3824 path = Path([p for c, p in patch_path], [c for c, p in patch_path])
3826 return path, True
3828 @_register_style(_style_list)
3829 class Wedge(_Base):
3830 """
3831 Wedge(?) shape. Only works with a quadratic Bezier curve. The
3832 begin point has a width of the tail_width and the end point has a
3833 width of 0. At the middle, the width is shrink_factor*tail_width.
3834 """
3836 def __init__(self, tail_width=.3, shrink_factor=0.5):
3837 """
3838 Parameters
3839 ----------
3840 tail_width : float, optional, default : 0.3
3841 Width of the tail
3843 shrink_factor : float, optional, default : 0.5
3844 Fraction of the arrow width at the middle point
3845 """
3846 self.tail_width = tail_width
3847 self.shrink_factor = shrink_factor
3848 super().__init__()
3850 def transmute(self, path, mutation_size, linewidth):
3852 x0, y0, x1, y1, x2, y2 = self.ensure_quadratic_bezier(path)
3854 arrow_path = [(x0, y0), (x1, y1), (x2, y2)]
3855 b_plus, b_minus = make_wedged_bezier2(
3856 arrow_path,
3857 self.tail_width * mutation_size / 2.,
3858 wm=self.shrink_factor)
3860 patch_path = [(Path.MOVETO, b_plus[0]),
3861 (Path.CURVE3, b_plus[1]),
3862 (Path.CURVE3, b_plus[2]),
3863 (Path.LINETO, b_minus[2]),
3864 (Path.CURVE3, b_minus[1]),
3865 (Path.CURVE3, b_minus[0]),
3866 (Path.CLOSEPOLY, b_minus[0]),
3867 ]
3868 path = Path([p for c, p in patch_path], [c for c, p in patch_path])
3870 return path, True
3872 if __doc__:
3873 __doc__ = inspect.cleandoc(__doc__) % {
3874 "AvailableArrowstyles": _pprint_styles(_style_list)}
3877docstring.interpd.update(
3878 AvailableArrowstyles=_pprint_styles(ArrowStyle._style_list),
3879 AvailableConnectorstyles=_pprint_styles(ConnectionStyle._style_list),
3880)
3883class FancyArrowPatch(Patch):
3884 """
3885 A fancy arrow patch. It draws an arrow using the :class:`ArrowStyle`.
3887 The head and tail positions are fixed at the specified start and end points
3888 of the arrow, but the size and shape (in display coordinates) of the arrow
3889 does not change when the axis is moved or zoomed.
3890 """
3891 _edge_default = True
3893 def __str__(self):
3895 if self._posA_posB is not None:
3896 (x1, y1), (x2, y2) = self._posA_posB
3897 return self.__class__.__name__ \
3898 + "((%g, %g)->(%g, %g))" % (x1, y1, x2, y2)
3899 else:
3900 return self.__class__.__name__ \
3901 + "(%s)" % (str(self._path_original),)
3903 @docstring.dedent_interpd
3904 def __init__(self, posA=None, posB=None,
3905 path=None,
3906 arrowstyle="simple",
3907 arrow_transmuter=None,
3908 connectionstyle="arc3",
3909 connector=None,
3910 patchA=None,
3911 patchB=None,
3912 shrinkA=2,
3913 shrinkB=2,
3914 mutation_scale=1,
3915 mutation_aspect=None,
3916 dpi_cor=1,
3917 **kwargs):
3918 """
3919 There are two ways for defining an arrow:
3921 - If *posA* and *posB* are given, a path connecting two points is
3922 created according to *connectionstyle*. The path will be
3923 clipped with *patchA* and *patchB* and further shrunken by
3924 *shrinkA* and *shrinkB*. An arrow is drawn along this
3925 resulting path using the *arrowstyle* parameter.
3927 - Alternatively if *path* is provided, an arrow is drawn along this
3928 path and *patchA*, *patchB*, *shrinkA*, and *shrinkB* are ignored.
3930 Parameters
3931 ----------
3932 posA, posB : (float, float), optional (default: None)
3933 (x, y) coordinates of arrow tail and arrow head respectively.
3935 path : `~matplotlib.path.Path`, optional (default: None)
3936 If provided, an arrow is drawn along this path and *patchA*,
3937 *patchB*, *shrinkA*, and *shrinkB* are ignored.
3939 arrowstyle : str or `.ArrowStyle`, optional (default: 'simple')
3940 Describes how the fancy arrow will be
3941 drawn. It can be string of the available arrowstyle names,
3942 with optional comma-separated attributes, or an
3943 :class:`ArrowStyle` instance. The optional attributes are meant to
3944 be scaled with the *mutation_scale*. The following arrow styles are
3945 available:
3947 %(AvailableArrowstyles)s
3949 arrow_transmuter
3950 Ignored.
3952 connectionstyle : str or `.ConnectionStyle` or None, optional \
3953(default: 'arc3')
3954 Describes how *posA* and *posB* are connected. It can be an
3955 instance of the :class:`ConnectionStyle` class or a string of the
3956 connectionstyle name, with optional comma-separated attributes. The
3957 following connection styles are available:
3959 %(AvailableConnectorstyles)s
3961 connector
3962 Ignored.
3964 patchA, patchB : `.Patch`, optional (default: None)
3965 Head and tail patch respectively. :class:`matplotlib.patch.Patch`
3966 instance.
3968 shrinkA, shrinkB : float, optional (default: 2)
3969 Shrinking factor of the tail and head of the arrow respectively.
3971 mutation_scale : float, optional (default: 1)
3972 Value with which attributes of *arrowstyle* (e.g., *head_length*)
3973 will be scaled.
3975 mutation_aspect : None or float, optional (default: None)
3976 The height of the rectangle will be squeezed by this value before
3977 the mutation and the mutated box will be stretched by the inverse
3978 of it.
3980 dpi_cor : float, optional (default: 1)
3981 dpi_cor is currently used for linewidth-related things and shrink
3982 factor. Mutation scale is affected by this.
3984 Other Parameters
3985 ----------------
3986 **kwargs : `.Patch` properties, optional
3987 Here is a list of available `.Patch` properties:
3989 %(Patch)s
3991 In contrast to other patches, the default ``capstyle`` and
3992 ``joinstyle`` for `FancyArrowPatch` are set to ``"round"``.
3993 """
3994 if arrow_transmuter is not None:
3995 cbook.warn_deprecated(
3996 3.0,
3997 message=('The "arrow_transmuter" keyword argument is not used,'
3998 ' and will be removed in Matplotlib 3.1'),
3999 name='arrow_transmuter',
4000 obj_type='keyword argument')
4001 if connector is not None:
4002 cbook.warn_deprecated(
4003 3.0,
4004 message=('The "connector" keyword argument is not used,'
4005 ' and will be removed in Matplotlib 3.1'),
4006 name='connector',
4007 obj_type='keyword argument')
4008 # Traditionally, the cap- and joinstyle for FancyArrowPatch are round
4009 kwargs.setdefault("joinstyle", "round")
4010 kwargs.setdefault("capstyle", "round")
4012 Patch.__init__(self, **kwargs)
4014 if posA is not None and posB is not None and path is None:
4015 self._posA_posB = [posA, posB]
4017 if connectionstyle is None:
4018 connectionstyle = "arc3"
4019 self.set_connectionstyle(connectionstyle)
4021 elif posA is None and posB is None and path is not None:
4022 self._posA_posB = None
4023 else:
4024 raise ValueError("either posA and posB, or path need to provided")
4026 self.patchA = patchA
4027 self.patchB = patchB
4028 self.shrinkA = shrinkA
4029 self.shrinkB = shrinkB
4031 self._path_original = path
4033 self.set_arrowstyle(arrowstyle)
4035 self._mutation_scale = mutation_scale
4036 self._mutation_aspect = mutation_aspect
4038 self.set_dpi_cor(dpi_cor)
4040 def set_dpi_cor(self, dpi_cor):
4041 """
4042 dpi_cor is currently used for linewidth-related things and
4043 shrink factor. Mutation scale is affected by this.
4045 Parameters
4046 ----------
4047 dpi_cor : scalar
4048 """
4049 self._dpi_cor = dpi_cor
4050 self.stale = True
4052 def get_dpi_cor(self):
4053 """
4054 dpi_cor is currently used for linewidth-related things and
4055 shrink factor. Mutation scale is affected by this.
4057 Returns
4058 -------
4059 dpi_cor : scalar
4060 """
4061 return self._dpi_cor
4063 def set_positions(self, posA, posB):
4064 """
4065 Set the begin and end positions of the connecting path.
4067 Parameters
4068 ----------
4069 posA, posB : None, tuple
4070 (x, y) coordinates of arrow tail and arrow head respectively. If
4071 `None` use current value.
4072 """
4073 if posA is not None:
4074 self._posA_posB[0] = posA
4075 if posB is not None:
4076 self._posA_posB[1] = posB
4077 self.stale = True
4079 def set_patchA(self, patchA):
4080 """
4081 Set the tail patch.
4083 Parameters
4084 ----------
4085 patchA : Patch
4086 :class:`matplotlib.patch.Patch` instance.
4087 """
4088 self.patchA = patchA
4089 self.stale = True
4091 def set_patchB(self, patchB):
4092 """
4093 Set the head patch.
4095 Parameters
4096 ----------
4097 patchB : Patch
4098 :class:`matplotlib.patch.Patch` instance.
4099 """
4100 self.patchB = patchB
4101 self.stale = True
4103 def set_connectionstyle(self, connectionstyle, **kw):
4104 """
4105 Set the connection style. Old attributes are forgotten.
4107 Parameters
4108 ----------
4109 connectionstyle : str or `.ConnectionStyle` or None, optional
4110 Can be a string with connectionstyle name with
4111 optional comma-separated attributes, e.g.::
4113 set_connectionstyle("arc,angleA=0,armA=30,rad=10")
4115 Alternatively, the attributes can be provided as keywords, e.g.::
4117 set_connectionstyle("arc", angleA=0,armA=30,rad=10)
4119 Without any arguments (or with ``connectionstyle=None``), return
4120 available styles as a list of strings.
4121 """
4123 if connectionstyle is None:
4124 return ConnectionStyle.pprint_styles()
4126 if (isinstance(connectionstyle, ConnectionStyle._Base) or
4127 callable(connectionstyle)):
4128 self._connector = connectionstyle
4129 else:
4130 self._connector = ConnectionStyle(connectionstyle, **kw)
4131 self.stale = True
4133 def get_connectionstyle(self):
4134 """
4135 Return the :class:`ConnectionStyle` instance.
4136 """
4137 return self._connector
4139 def set_arrowstyle(self, arrowstyle=None, **kw):
4140 """
4141 Set the arrow style. Old attributes are forgotten. Without arguments
4142 (or with ``arrowstyle=None``) returns available box styles as a list of
4143 strings.
4145 Parameters
4146 ----------
4147 arrowstyle : None, ArrowStyle, str, optional (default: None)
4148 Can be a string with arrowstyle name with optional comma-separated
4149 attributes, e.g.::
4151 set_arrowstyle("Fancy,head_length=0.2")
4153 Alternatively attributes can be provided as keywords, e.g.::
4155 set_arrowstyle("fancy", head_length=0.2)
4157 """
4159 if arrowstyle is None:
4160 return ArrowStyle.pprint_styles()
4162 if isinstance(arrowstyle, ArrowStyle._Base):
4163 self._arrow_transmuter = arrowstyle
4164 else:
4165 self._arrow_transmuter = ArrowStyle(arrowstyle, **kw)
4166 self.stale = True
4168 def get_arrowstyle(self):
4169 """
4170 Return the arrowstyle object.
4171 """
4172 return self._arrow_transmuter
4174 def set_mutation_scale(self, scale):
4175 """
4176 Set the mutation scale.
4178 Parameters
4179 ----------
4180 scale : scalar
4181 """
4182 self._mutation_scale = scale
4183 self.stale = True
4185 def get_mutation_scale(self):
4186 """
4187 Return the mutation scale.
4189 Returns
4190 -------
4191 scale : scalar
4192 """
4193 return self._mutation_scale
4195 def set_mutation_aspect(self, aspect):
4196 """
4197 Set the aspect ratio of the bbox mutation.
4199 Parameters
4200 ----------
4201 aspect : scalar
4202 """
4203 self._mutation_aspect = aspect
4204 self.stale = True
4206 def get_mutation_aspect(self):
4207 """
4208 Return the aspect ratio of the bbox mutation.
4209 """
4210 return self._mutation_aspect
4212 def get_path(self):
4213 """
4214 Return the path of the arrow in the data coordinates. Use
4215 get_path_in_displaycoord() method to retrieve the arrow path
4216 in display coordinates.
4217 """
4218 _path, fillable = self.get_path_in_displaycoord()
4219 if np.iterable(fillable):
4220 _path = concatenate_paths(_path)
4221 return self.get_transform().inverted().transform_path(_path)
4223 def get_path_in_displaycoord(self):
4224 """
4225 Return the mutated path of the arrow in display coordinates.
4226 """
4228 dpi_cor = self.get_dpi_cor()
4230 if self._posA_posB is not None:
4231 posA = self._convert_xy_units(self._posA_posB[0])
4232 posB = self._convert_xy_units(self._posA_posB[1])
4233 (posA, posB) = self.get_transform().transform((posA, posB))
4234 _path = self.get_connectionstyle()(posA, posB,
4235 patchA=self.patchA,
4236 patchB=self.patchB,
4237 shrinkA=self.shrinkA * dpi_cor,
4238 shrinkB=self.shrinkB * dpi_cor
4239 )
4240 else:
4241 _path = self.get_transform().transform_path(self._path_original)
4243 _path, fillable = self.get_arrowstyle()(
4244 _path,
4245 self.get_mutation_scale() * dpi_cor,
4246 self.get_linewidth() * dpi_cor,
4247 self.get_mutation_aspect())
4249 # if not fillable:
4250 # self._fill = False
4252 return _path, fillable
4254 def draw(self, renderer):
4255 if not self.get_visible():
4256 return
4258 with self._bind_draw_path_function(renderer) as draw_path:
4260 # FIXME : dpi_cor is for the dpi-dependency of the linewidth. There
4261 # could be room for improvement.
4262 self.set_dpi_cor(renderer.points_to_pixels(1.))
4263 path, fillable = self.get_path_in_displaycoord()
4265 if not np.iterable(fillable):
4266 path = [path]
4267 fillable = [fillable]
4269 affine = transforms.IdentityTransform()
4271 for p, f in zip(path, fillable):
4272 draw_path(
4273 p, affine,
4274 self._facecolor if f and self._facecolor[3] else None)
4277class ConnectionPatch(FancyArrowPatch):
4278 """
4279 A :class:`~matplotlib.patches.ConnectionPatch` class is to make
4280 connecting lines between two points (possibly in different axes).
4281 """
4282 def __str__(self):
4283 return "ConnectionPatch((%g, %g), (%g, %g))" % \
4284 (self.xy1[0], self.xy1[1], self.xy2[0], self.xy2[1])
4286 @docstring.dedent_interpd
4287 def __init__(self, xyA, xyB, coordsA, coordsB=None,
4288 axesA=None, axesB=None,
4289 arrowstyle="-",
4290 arrow_transmuter=None,
4291 connectionstyle="arc3",
4292 connector=None,
4293 patchA=None,
4294 patchB=None,
4295 shrinkA=0.,
4296 shrinkB=0.,
4297 mutation_scale=10.,
4298 mutation_aspect=None,
4299 clip_on=False,
4300 dpi_cor=1.,
4301 **kwargs):
4302 """Connect point *xyA* in *coordsA* with point *xyB* in *coordsB*
4304 Valid keys are
4306 =============== ======================================================
4307 Key Description
4308 =============== ======================================================
4309 arrowstyle the arrow style
4310 connectionstyle the connection style
4311 relpos default is (0.5, 0.5)
4312 patchA default is bounding box of the text
4313 patchB default is None
4314 shrinkA default is 2 points
4315 shrinkB default is 2 points
4316 mutation_scale default is text size (in points)
4317 mutation_aspect default is 1.
4318 ? any key for :class:`matplotlib.patches.PathPatch`
4319 =============== ======================================================
4321 *coordsA* and *coordsB* are strings that indicate the
4322 coordinates of *xyA* and *xyB*.
4324 ================= ===================================================
4325 Property Description
4326 ================= ===================================================
4327 'figure points' points from the lower left corner of the figure
4328 'figure pixels' pixels from the lower left corner of the figure
4329 'figure fraction' 0, 0 is lower left of figure and 1, 1 is upper right
4330 'axes points' points from lower left corner of axes
4331 'axes pixels' pixels from lower left corner of axes
4332 'axes fraction' 0, 0 is lower left of axes and 1, 1 is upper right
4333 'data' use the coordinate system of the object being
4334 annotated (default)
4335 'offset points' offset (in points) from the *xy* value
4336 'polar' you can specify *theta*, *r* for the annotation,
4337 even in cartesian plots. Note that if you are using
4338 a polar axes, you do not need to specify polar for
4339 the coordinate system since that is the native
4340 "data" coordinate system.
4341 ================= ===================================================
4343 Alternatively they can be set to any valid
4344 `~matplotlib.transforms.Transform`.
4346 .. note::
4348 Using :class:`~matplotlib.patches.ConnectionPatch` across
4349 two :class:`~matplotlib.axes.Axes` instances is not
4350 directly compatible with :doc:`constrained layout
4351 </tutorials/intermediate/constrainedlayout_guide>`. Add the
4352 artist directly to the :class:`~matplotlib.figure.Figure`
4353 instead of adding it to a specific Axes.
4355 .. code-block:: default
4357 fig, ax = plt.subplots(1, 2, constrained_layout=True)
4358 con = ConnectionPatch(..., axesA=ax[0], axesB=ax[1])
4359 fig.add_artist(con)
4361 """
4362 if coordsB is None:
4363 coordsB = coordsA
4364 # we'll draw ourself after the artist we annotate by default
4365 self.xy1 = xyA
4366 self.xy2 = xyB
4367 self.coords1 = coordsA
4368 self.coords2 = coordsB
4370 self.axesA = axesA
4371 self.axesB = axesB
4373 FancyArrowPatch.__init__(self,
4374 posA=(0, 0), posB=(1, 1),
4375 arrowstyle=arrowstyle,
4376 arrow_transmuter=arrow_transmuter,
4377 connectionstyle=connectionstyle,
4378 connector=connector,
4379 patchA=patchA,
4380 patchB=patchB,
4381 shrinkA=shrinkA,
4382 shrinkB=shrinkB,
4383 mutation_scale=mutation_scale,
4384 mutation_aspect=mutation_aspect,
4385 clip_on=clip_on,
4386 dpi_cor=dpi_cor,
4387 **kwargs)
4389 # if True, draw annotation only if self.xy is inside the axes
4390 self._annotation_clip = None
4392 def _get_xy(self, x, y, s, axes=None):
4393 """Calculate the pixel position of given point."""
4394 if axes is None:
4395 axes = self.axes
4397 if s == 'data':
4398 trans = axes.transData
4399 x = float(self.convert_xunits(x))
4400 y = float(self.convert_yunits(y))
4401 return trans.transform((x, y))
4402 elif s == 'offset points':
4403 # convert the data point
4404 dx, dy = self.xy
4406 # prevent recursion
4407 if self.xycoords == 'offset points':
4408 return self._get_xy(dx, dy, 'data')
4410 dx, dy = self._get_xy(dx, dy, self.xycoords)
4412 # convert the offset
4413 dpi = self.figure.get_dpi()
4414 x *= dpi / 72.
4415 y *= dpi / 72.
4417 # add the offset to the data point
4418 x += dx
4419 y += dy
4421 return x, y
4422 elif s == 'polar':
4423 theta, r = x, y
4424 x = r * np.cos(theta)
4425 y = r * np.sin(theta)
4426 trans = axes.transData
4427 return trans.transform((x, y))
4428 elif s == 'figure points':
4429 # points from the lower left corner of the figure
4430 dpi = self.figure.dpi
4431 l, b, w, h = self.figure.bbox.bounds
4432 r = l + w
4433 t = b + h
4435 x *= dpi / 72.
4436 y *= dpi / 72.
4437 if x < 0:
4438 x = r + x
4439 if y < 0:
4440 y = t + y
4441 return x, y
4442 elif s == 'figure pixels':
4443 # pixels from the lower left corner of the figure
4444 l, b, w, h = self.figure.bbox.bounds
4445 r = l + w
4446 t = b + h
4447 if x < 0:
4448 x = r + x
4449 if y < 0:
4450 y = t + y
4451 return x, y
4452 elif s == 'figure fraction':
4453 # (0, 0) is lower left, (1, 1) is upper right of figure
4454 trans = self.figure.transFigure
4455 return trans.transform((x, y))
4456 elif s == 'axes points':
4457 # points from the lower left corner of the axes
4458 dpi = self.figure.dpi
4459 l, b, w, h = axes.bbox.bounds
4460 r = l + w
4461 t = b + h
4462 if x < 0:
4463 x = r + x * dpi / 72.
4464 else:
4465 x = l + x * dpi / 72.
4466 if y < 0:
4467 y = t + y * dpi / 72.
4468 else:
4469 y = b + y * dpi / 72.
4470 return x, y
4471 elif s == 'axes pixels':
4472 # pixels from the lower left corner of the axes
4473 l, b, w, h = axes.bbox.bounds
4474 r = l + w
4475 t = b + h
4476 if x < 0:
4477 x = r + x
4478 else:
4479 x = l + x
4480 if y < 0:
4481 y = t + y
4482 else:
4483 y = b + y
4484 return x, y
4485 elif s == 'axes fraction':
4486 # (0, 0) is lower left, (1, 1) is upper right of axes
4487 trans = axes.transAxes
4488 return trans.transform((x, y))
4489 elif isinstance(s, transforms.Transform):
4490 return s.transform((x, y))
4491 else:
4492 raise ValueError("{} is not a valid coordinate "
4493 "transformation.".format(s))
4495 def set_annotation_clip(self, b):
4496 """
4497 Set the clipping behavior.
4499 Parameters
4500 ----------
4501 b : bool or None
4503 - *False*: The annotation will always be drawn regardless of its
4504 position.
4505 - *True*: The annotation will only be drawn if ``self.xy`` is
4506 inside the axes.
4507 - *None*: The annotation will only be drawn if ``self.xy`` is
4508 inside the axes and ``self.xycoords == "data"``.
4509 """
4510 self._annotation_clip = b
4511 self.stale = True
4513 def get_annotation_clip(self):
4514 """
4515 Return the clipping behavior.
4517 See `.set_annotation_clip` for the meaning of the return value.
4518 """
4519 return self._annotation_clip
4521 def get_path_in_displaycoord(self):
4522 """Return the mutated path of the arrow in display coordinates."""
4524 dpi_cor = self.get_dpi_cor()
4526 x, y = self.xy1
4527 posA = self._get_xy(x, y, self.coords1, self.axesA)
4529 x, y = self.xy2
4530 posB = self._get_xy(x, y, self.coords2, self.axesB)
4532 _path = self.get_connectionstyle()(posA, posB,
4533 patchA=self.patchA,
4534 patchB=self.patchB,
4535 shrinkA=self.shrinkA * dpi_cor,
4536 shrinkB=self.shrinkB * dpi_cor
4537 )
4539 _path, fillable = self.get_arrowstyle()(
4540 _path,
4541 self.get_mutation_scale() * dpi_cor,
4542 self.get_linewidth() * dpi_cor,
4543 self.get_mutation_aspect()
4544 )
4546 return _path, fillable
4548 def _check_xy(self, renderer):
4549 """Check whether the annotation needs to be drawn."""
4551 b = self.get_annotation_clip()
4553 if b or (b is None and self.coords1 == "data"):
4554 x, y = self.xy1
4555 xy_pixel = self._get_xy(x, y, self.coords1, self.axesA)
4556 if self.axesA is None:
4557 axes = self.axes
4558 else:
4559 axes = self.axesA
4560 if not axes.contains_point(xy_pixel):
4561 return False
4563 if b or (b is None and self.coords2 == "data"):
4564 x, y = self.xy2
4565 xy_pixel = self._get_xy(x, y, self.coords2, self.axesB)
4566 if self.axesB is None:
4567 axes = self.axes
4568 else:
4569 axes = self.axesB
4570 if not axes.contains_point(xy_pixel):
4571 return False
4573 return True
4575 def draw(self, renderer):
4576 if renderer is not None:
4577 self._renderer = renderer
4578 if not self.get_visible() or not self._check_xy(renderer):
4579 return
4580 FancyArrowPatch.draw(self, renderer)