1r"""
2Container classes for `.Artist`\s.
3
4`OffsetBox`
5 The base of all container artists defined in this module.
6
7`AnchoredOffsetbox`, `AnchoredText`
8 Anchor and align an arbitrary `.Artist` or a text relative to the parent
9 axes or a specific anchor point.
10
11`DrawingArea`
12 A container with fixed width and height. Children have a fixed position
13 inside the container and may be clipped.
14
15`HPacker`, `VPacker`
16 Containers for layouting their children vertically or horizontally.
17
18`PaddedBox`
19 A container to add a padding around an `.Artist`.
20
21`TextArea`
22 Contains a single `.Text` instance.
23"""
24
25import functools
26
27import numpy as np
28
29import matplotlib as mpl
30from matplotlib import _api, _docstring
31import matplotlib.artist as martist
32import matplotlib.path as mpath
33import matplotlib.text as mtext
34import matplotlib.transforms as mtransforms
35from matplotlib.font_manager import FontProperties
36from matplotlib.image import BboxImage
37from matplotlib.patches import (
38 FancyBboxPatch, FancyArrowPatch, bbox_artist as mbbox_artist)
39from matplotlib.transforms import Bbox, BboxBase, TransformedBbox
40
41
42DEBUG = False
43
44
45def _compat_get_offset(meth):
46 """
47 Decorator for the get_offset method of OffsetBox and subclasses, that
48 allows supporting both the new signature (self, bbox, renderer) and the old
49 signature (self, width, height, xdescent, ydescent, renderer).
50 """
51 sigs = [lambda self, width, height, xdescent, ydescent, renderer: locals(),
52 lambda self, bbox, renderer: locals()]
53
54 @functools.wraps(meth)
55 def get_offset(self, *args, **kwargs):
56 params = _api.select_matching_signature(sigs, self, *args, **kwargs)
57 bbox = (params["bbox"] if "bbox" in params else
58 Bbox.from_bounds(-params["xdescent"], -params["ydescent"],
59 params["width"], params["height"]))
60 return meth(params["self"], bbox, params["renderer"])
61 return get_offset
62
63
64# for debugging use
65def _bbox_artist(*args, **kwargs):
66 if DEBUG:
67 mbbox_artist(*args, **kwargs)
68
69
70def _get_packed_offsets(widths, total, sep, mode="fixed"):
71 r"""
72 Pack boxes specified by their *widths*.
73
74 For simplicity of the description, the terminology used here assumes a
75 horizontal layout, but the function works equally for a vertical layout.
76
77 There are three packing *mode*\s:
78
79 - 'fixed': The elements are packed tight to the left with a spacing of
80 *sep* in between. If *total* is *None* the returned total will be the
81 right edge of the last box. A non-*None* total will be passed unchecked
82 to the output. In particular this means that right edge of the last
83 box may be further to the right than the returned total.
84
85 - 'expand': Distribute the boxes with equal spacing so that the left edge
86 of the first box is at 0, and the right edge of the last box is at
87 *total*. The parameter *sep* is ignored in this mode. A total of *None*
88 is accepted and considered equal to 1. The total is returned unchanged
89 (except for the conversion *None* to 1). If the total is smaller than
90 the sum of the widths, the laid out boxes will overlap.
91
92 - 'equal': If *total* is given, the total space is divided in N equal
93 ranges and each box is left-aligned within its subspace.
94 Otherwise (*total* is *None*), *sep* must be provided and each box is
95 left-aligned in its subspace of width ``(max(widths) + sep)``. The
96 total width is then calculated to be ``N * (max(widths) + sep)``.
97
98 Parameters
99 ----------
100 widths : list of float
101 Widths of boxes to be packed.
102 total : float or None
103 Intended total length. *None* if not used.
104 sep : float or None
105 Spacing between boxes.
106 mode : {'fixed', 'expand', 'equal'}
107 The packing mode.
108
109 Returns
110 -------
111 total : float
112 The total width needed to accommodate the laid out boxes.
113 offsets : array of float
114 The left offsets of the boxes.
115 """
116 _api.check_in_list(["fixed", "expand", "equal"], mode=mode)
117
118 if mode == "fixed":
119 offsets_ = np.cumsum([0] + [w + sep for w in widths])
120 offsets = offsets_[:-1]
121 if total is None:
122 total = offsets_[-1] - sep
123 return total, offsets
124
125 elif mode == "expand":
126 # This is a bit of a hack to avoid a TypeError when *total*
127 # is None and used in conjugation with tight layout.
128 if total is None:
129 total = 1
130 if len(widths) > 1:
131 sep = (total - sum(widths)) / (len(widths) - 1)
132 else:
133 sep = 0
134 offsets_ = np.cumsum([0] + [w + sep for w in widths])
135 offsets = offsets_[:-1]
136 return total, offsets
137
138 elif mode == "equal":
139 maxh = max(widths)
140 if total is None:
141 if sep is None:
142 raise ValueError("total and sep cannot both be None when "
143 "using layout mode 'equal'")
144 total = (maxh + sep) * len(widths)
145 else:
146 sep = total / len(widths) - maxh
147 offsets = (maxh + sep) * np.arange(len(widths))
148 return total, offsets
149
150
151def _get_aligned_offsets(yspans, height, align="baseline"):
152 """
153 Align boxes each specified by their ``(y0, y1)`` spans.
154
155 For simplicity of the description, the terminology used here assumes a
156 horizontal layout (i.e., vertical alignment), but the function works
157 equally for a vertical layout.
158
159 Parameters
160 ----------
161 yspans
162 List of (y0, y1) spans of boxes to be aligned.
163 height : float or None
164 Intended total height. If None, the maximum of the heights
165 (``y1 - y0``) in *yspans* is used.
166 align : {'baseline', 'left', 'top', 'right', 'bottom', 'center'}
167 The alignment anchor of the boxes.
168
169 Returns
170 -------
171 (y0, y1)
172 y range spanned by the packing. If a *height* was originally passed
173 in, then for all alignments other than "baseline", a span of ``(0,
174 height)`` is used without checking that it is actually large enough).
175 descent
176 The descent of the packing.
177 offsets
178 The bottom offsets of the boxes.
179 """
180
181 _api.check_in_list(
182 ["baseline", "left", "top", "right", "bottom", "center"], align=align)
183 if height is None:
184 height = max(y1 - y0 for y0, y1 in yspans)
185
186 if align == "baseline":
187 yspan = (min(y0 for y0, y1 in yspans), max(y1 for y0, y1 in yspans))
188 offsets = [0] * len(yspans)
189 elif align in ["left", "bottom"]:
190 yspan = (0, height)
191 offsets = [-y0 for y0, y1 in yspans]
192 elif align in ["right", "top"]:
193 yspan = (0, height)
194 offsets = [height - y1 for y0, y1 in yspans]
195 elif align == "center":
196 yspan = (0, height)
197 offsets = [(height - (y1 - y0)) * .5 - y0 for y0, y1 in yspans]
198
199 return yspan, offsets
200
201
202class OffsetBox(martist.Artist):
203 """
204 The OffsetBox is a simple container artist.
205
206 The child artists are meant to be drawn at a relative position to its
207 parent.
208
209 Being an artist itself, all parameters are passed on to `.Artist`.
210 """
211 def __init__(self, *args, **kwargs):
212 super().__init__(*args)
213 self._internal_update(kwargs)
214 # Clipping has not been implemented in the OffsetBox family, so
215 # disable the clip flag for consistency. It can always be turned back
216 # on to zero effect.
217 self.set_clip_on(False)
218 self._children = []
219 self._offset = (0, 0)
220
221 def set_figure(self, fig):
222 """
223 Set the `.Figure` for the `.OffsetBox` and all its children.
224
225 Parameters
226 ----------
227 fig : `~matplotlib.figure.Figure`
228 """
229 super().set_figure(fig)
230 for c in self.get_children():
231 c.set_figure(fig)
232
233 @martist.Artist.axes.setter
234 def axes(self, ax):
235 # TODO deal with this better
236 martist.Artist.axes.fset(self, ax)
237 for c in self.get_children():
238 if c is not None:
239 c.axes = ax
240
241 def contains(self, mouseevent):
242 """
243 Delegate the mouse event contains-check to the children.
244
245 As a container, the `.OffsetBox` does not respond itself to
246 mouseevents.
247
248 Parameters
249 ----------
250 mouseevent : `~matplotlib.backend_bases.MouseEvent`
251
252 Returns
253 -------
254 contains : bool
255 Whether any values are within the radius.
256 details : dict
257 An artist-specific dictionary of details of the event context,
258 such as which points are contained in the pick radius. See the
259 individual Artist subclasses for details.
260
261 See Also
262 --------
263 .Artist.contains
264 """
265 if self._different_canvas(mouseevent):
266 return False, {}
267 for c in self.get_children():
268 a, b = c.contains(mouseevent)
269 if a:
270 return a, b
271 return False, {}
272
273 def set_offset(self, xy):
274 """
275 Set the offset.
276
277 Parameters
278 ----------
279 xy : (float, float) or callable
280 The (x, y) coordinates of the offset in display units. These can
281 either be given explicitly as a tuple (x, y), or by providing a
282 function that converts the extent into the offset. This function
283 must have the signature::
284
285 def offset(width, height, xdescent, ydescent, renderer) \
286-> (float, float)
287 """
288 self._offset = xy
289 self.stale = True
290
291 @_compat_get_offset
292 def get_offset(self, bbox, renderer):
293 """
294 Return the offset as a tuple (x, y).
295
296 The extent parameters have to be provided to handle the case where the
297 offset is dynamically determined by a callable (see
298 `~.OffsetBox.set_offset`).
299
300 Parameters
301 ----------
302 bbox : `.Bbox`
303 renderer : `.RendererBase` subclass
304 """
305 return (
306 self._offset(bbox.width, bbox.height, -bbox.x0, -bbox.y0, renderer)
307 if callable(self._offset)
308 else self._offset)
309
310 def set_width(self, width):
311 """
312 Set the width of the box.
313
314 Parameters
315 ----------
316 width : float
317 """
318 self.width = width
319 self.stale = True
320
321 def set_height(self, height):
322 """
323 Set the height of the box.
324
325 Parameters
326 ----------
327 height : float
328 """
329 self.height = height
330 self.stale = True
331
332 def get_visible_children(self):
333 r"""Return a list of the visible child `.Artist`\s."""
334 return [c for c in self._children if c.get_visible()]
335
336 def get_children(self):
337 r"""Return a list of the child `.Artist`\s."""
338 return self._children
339
340 def _get_bbox_and_child_offsets(self, renderer):
341 """
342 Return the bbox of the offsetbox and the child offsets.
343
344 The bbox should satisfy ``x0 <= x1 and y0 <= y1``.
345
346 Parameters
347 ----------
348 renderer : `.RendererBase` subclass
349
350 Returns
351 -------
352 bbox
353 list of (xoffset, yoffset) pairs
354 """
355 raise NotImplementedError(
356 "get_bbox_and_offsets must be overridden in derived classes")
357
358 def get_bbox(self, renderer):
359 """Return the bbox of the offsetbox, ignoring parent offsets."""
360 bbox, offsets = self._get_bbox_and_child_offsets(renderer)
361 return bbox
362
363 def get_window_extent(self, renderer=None):
364 # docstring inherited
365 if renderer is None:
366 renderer = self.figure._get_renderer()
367 bbox = self.get_bbox(renderer)
368 try: # Some subclasses redefine get_offset to take no args.
369 px, py = self.get_offset(bbox, renderer)
370 except TypeError:
371 px, py = self.get_offset()
372 return bbox.translated(px, py)
373
374 def draw(self, renderer):
375 """
376 Update the location of children if necessary and draw them
377 to the given *renderer*.
378 """
379 bbox, offsets = self._get_bbox_and_child_offsets(renderer)
380 px, py = self.get_offset(bbox, renderer)
381 for c, (ox, oy) in zip(self.get_visible_children(), offsets):
382 c.set_offset((px + ox, py + oy))
383 c.draw(renderer)
384 _bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
385 self.stale = False
386
387
388class PackerBase(OffsetBox):
389 def __init__(self, pad=0., sep=0., width=None, height=None,
390 align="baseline", mode="fixed", children=None):
391 """
392 Parameters
393 ----------
394 pad : float, default: 0.0
395 The boundary padding in points.
396
397 sep : float, default: 0.0
398 The spacing between items in points.
399
400 width, height : float, optional
401 Width and height of the container box in pixels, calculated if
402 *None*.
403
404 align : {'top', 'bottom', 'left', 'right', 'center', 'baseline'}, \
405default: 'baseline'
406 Alignment of boxes.
407
408 mode : {'fixed', 'expand', 'equal'}, default: 'fixed'
409 The packing mode.
410
411 - 'fixed' packs the given `.Artist`\\s tight with *sep* spacing.
412 - 'expand' uses the maximal available space to distribute the
413 artists with equal spacing in between.
414 - 'equal': Each artist an equal fraction of the available space
415 and is left-aligned (or top-aligned) therein.
416
417 children : list of `.Artist`
418 The artists to pack.
419
420 Notes
421 -----
422 *pad* and *sep* are in points and will be scaled with the renderer
423 dpi, while *width* and *height* are in pixels.
424 """
425 super().__init__()
426 self.height = height
427 self.width = width
428 self.sep = sep
429 self.pad = pad
430 self.mode = mode
431 self.align = align
432 self._children = children
433
434
435class VPacker(PackerBase):
436 """
437 VPacker packs its children vertically, automatically adjusting their
438 relative positions at draw time.
439 """
440
441 def _get_bbox_and_child_offsets(self, renderer):
442 # docstring inherited
443 dpicor = renderer.points_to_pixels(1.)
444 pad = self.pad * dpicor
445 sep = self.sep * dpicor
446
447 if self.width is not None:
448 for c in self.get_visible_children():
449 if isinstance(c, PackerBase) and c.mode == "expand":
450 c.set_width(self.width)
451
452 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
453 (x0, x1), xoffsets = _get_aligned_offsets(
454 [bbox.intervalx for bbox in bboxes], self.width, self.align)
455 height, yoffsets = _get_packed_offsets(
456 [bbox.height for bbox in bboxes], self.height, sep, self.mode)
457
458 yoffsets = height - (yoffsets + [bbox.y1 for bbox in bboxes])
459 ydescent = yoffsets[0]
460 yoffsets = yoffsets - ydescent
461
462 return (
463 Bbox.from_bounds(x0, -ydescent, x1 - x0, height).padded(pad),
464 [*zip(xoffsets, yoffsets)])
465
466
467class HPacker(PackerBase):
468 """
469 HPacker packs its children horizontally, automatically adjusting their
470 relative positions at draw time.
471 """
472
473 def _get_bbox_and_child_offsets(self, renderer):
474 # docstring inherited
475 dpicor = renderer.points_to_pixels(1.)
476 pad = self.pad * dpicor
477 sep = self.sep * dpicor
478
479 bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
480 if not bboxes:
481 return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []
482
483 (y0, y1), yoffsets = _get_aligned_offsets(
484 [bbox.intervaly for bbox in bboxes], self.height, self.align)
485 width, xoffsets = _get_packed_offsets(
486 [bbox.width for bbox in bboxes], self.width, sep, self.mode)
487
488 x0 = bboxes[0].x0
489 xoffsets -= ([bbox.x0 for bbox in bboxes] - x0)
490
491 return (Bbox.from_bounds(x0, y0, width, y1 - y0).padded(pad),
492 [*zip(xoffsets, yoffsets)])
493
494
495class PaddedBox(OffsetBox):
496 """
497 A container to add a padding around an `.Artist`.
498
499 The `.PaddedBox` contains a `.FancyBboxPatch` that is used to visualize
500 it when rendering.
501 """
502
503 def __init__(self, child, pad=0., *, draw_frame=False, patch_attrs=None):
504 """
505 Parameters
506 ----------
507 child : `~matplotlib.artist.Artist`
508 The contained `.Artist`.
509 pad : float, default: 0.0
510 The padding in points. This will be scaled with the renderer dpi.
511 In contrast, *width* and *height* are in *pixels* and thus not
512 scaled.
513 draw_frame : bool
514 Whether to draw the contained `.FancyBboxPatch`.
515 patch_attrs : dict or None
516 Additional parameters passed to the contained `.FancyBboxPatch`.
517 """
518 super().__init__()
519 self.pad = pad
520 self._children = [child]
521 self.patch = FancyBboxPatch(
522 xy=(0.0, 0.0), width=1., height=1.,
523 facecolor='w', edgecolor='k',
524 mutation_scale=1, # self.prop.get_size_in_points(),
525 snap=True,
526 visible=draw_frame,
527 boxstyle="square,pad=0",
528 )
529 if patch_attrs is not None:
530 self.patch.update(patch_attrs)
531
532 def _get_bbox_and_child_offsets(self, renderer):
533 # docstring inherited.
534 pad = self.pad * renderer.points_to_pixels(1.)
535 return (self._children[0].get_bbox(renderer).padded(pad), [(0, 0)])
536
537 def draw(self, renderer):
538 # docstring inherited
539 bbox, offsets = self._get_bbox_and_child_offsets(renderer)
540 px, py = self.get_offset(bbox, renderer)
541 for c, (ox, oy) in zip(self.get_visible_children(), offsets):
542 c.set_offset((px + ox, py + oy))
543
544 self.draw_frame(renderer)
545
546 for c in self.get_visible_children():
547 c.draw(renderer)
548
549 self.stale = False
550
551 def update_frame(self, bbox, fontsize=None):
552 self.patch.set_bounds(bbox.bounds)
553 if fontsize:
554 self.patch.set_mutation_scale(fontsize)
555 self.stale = True
556
557 def draw_frame(self, renderer):
558 # update the location and size of the legend
559 self.update_frame(self.get_window_extent(renderer))
560 self.patch.draw(renderer)
561
562
563class DrawingArea(OffsetBox):
564 """
565 The DrawingArea can contain any Artist as a child. The DrawingArea
566 has a fixed width and height. The position of children relative to
567 the parent is fixed. The children can be clipped at the
568 boundaries of the parent.
569 """
570
571 def __init__(self, width, height, xdescent=0., ydescent=0., clip=False):
572 """
573 Parameters
574 ----------
575 width, height : float
576 Width and height of the container box.
577 xdescent, ydescent : float
578 Descent of the box in x- and y-direction.
579 clip : bool
580 Whether to clip the children to the box.
581 """
582 super().__init__()
583 self.width = width
584 self.height = height
585 self.xdescent = xdescent
586 self.ydescent = ydescent
587 self._clip_children = clip
588 self.offset_transform = mtransforms.Affine2D()
589 self.dpi_transform = mtransforms.Affine2D()
590
591 @property
592 def clip_children(self):
593 """
594 If the children of this DrawingArea should be clipped
595 by DrawingArea bounding box.
596 """
597 return self._clip_children
598
599 @clip_children.setter
600 def clip_children(self, val):
601 self._clip_children = bool(val)
602 self.stale = True
603
604 def get_transform(self):
605 """
606 Return the `~matplotlib.transforms.Transform` applied to the children.
607 """
608 return self.dpi_transform + self.offset_transform
609
610 def set_transform(self, t):
611 """
612 set_transform is ignored.
613 """
614
615 def set_offset(self, xy):
616 """
617 Set the offset of the container.
618
619 Parameters
620 ----------
621 xy : (float, float)
622 The (x, y) coordinates of the offset in display units.
623 """
624 self._offset = xy
625 self.offset_transform.clear()
626 self.offset_transform.translate(xy[0], xy[1])
627 self.stale = True
628
629 def get_offset(self):
630 """Return offset of the container."""
631 return self._offset
632
633 def get_bbox(self, renderer):
634 # docstring inherited
635 dpi_cor = renderer.points_to_pixels(1.)
636 return Bbox.from_bounds(
637 -self.xdescent * dpi_cor, -self.ydescent * dpi_cor,
638 self.width * dpi_cor, self.height * dpi_cor)
639
640 def add_artist(self, a):
641 """Add an `.Artist` to the container box."""
642 self._children.append(a)
643 if not a.is_transform_set():
644 a.set_transform(self.get_transform())
645 if self.axes is not None:
646 a.axes = self.axes
647 fig = self.figure
648 if fig is not None:
649 a.set_figure(fig)
650
651 def draw(self, renderer):
652 # docstring inherited
653
654 dpi_cor = renderer.points_to_pixels(1.)
655 self.dpi_transform.clear()
656 self.dpi_transform.scale(dpi_cor)
657
658 # At this point the DrawingArea has a transform
659 # to the display space so the path created is
660 # good for clipping children
661 tpath = mtransforms.TransformedPath(
662 mpath.Path([[0, 0], [0, self.height],
663 [self.width, self.height],
664 [self.width, 0]]),
665 self.get_transform())
666 for c in self._children:
667 if self._clip_children and not (c.clipbox or c._clippath):
668 c.set_clip_path(tpath)
669 c.draw(renderer)
670
671 _bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
672 self.stale = False
673
674
675class TextArea(OffsetBox):
676 """
677 The TextArea is a container artist for a single Text instance.
678
679 The text is placed at (0, 0) with baseline+left alignment, by default. The
680 width and height of the TextArea instance is the width and height of its
681 child text.
682 """
683
684 def __init__(self, s,
685 *,
686 textprops=None,
687 multilinebaseline=False,
688 ):
689 """
690 Parameters
691 ----------
692 s : str
693 The text to be displayed.
694 textprops : dict, default: {}
695 Dictionary of keyword parameters to be passed to the `.Text`
696 instance in the TextArea.
697 multilinebaseline : bool, default: False
698 Whether the baseline for multiline text is adjusted so that it
699 is (approximately) center-aligned with single-line text.
700 """
701 if textprops is None:
702 textprops = {}
703 self._text = mtext.Text(0, 0, s, **textprops)
704 super().__init__()
705 self._children = [self._text]
706 self.offset_transform = mtransforms.Affine2D()
707 self._baseline_transform = mtransforms.Affine2D()
708 self._text.set_transform(self.offset_transform +
709 self._baseline_transform)
710 self._multilinebaseline = multilinebaseline
711
712 def set_text(self, s):
713 """Set the text of this area as a string."""
714 self._text.set_text(s)
715 self.stale = True
716
717 def get_text(self):
718 """Return the string representation of this area's text."""
719 return self._text.get_text()
720
721 def set_multilinebaseline(self, t):
722 """
723 Set multilinebaseline.
724
725 If True, the baseline for multiline text is adjusted so that it is
726 (approximately) center-aligned with single-line text. This is used
727 e.g. by the legend implementation so that single-line labels are
728 baseline-aligned, but multiline labels are "center"-aligned with them.
729 """
730 self._multilinebaseline = t
731 self.stale = True
732
733 def get_multilinebaseline(self):
734 """
735 Get multilinebaseline.
736 """
737 return self._multilinebaseline
738
739 def set_transform(self, t):
740 """
741 set_transform is ignored.
742 """
743
744 def set_offset(self, xy):
745 """
746 Set the offset of the container.
747
748 Parameters
749 ----------
750 xy : (float, float)
751 The (x, y) coordinates of the offset in display units.
752 """
753 self._offset = xy
754 self.offset_transform.clear()
755 self.offset_transform.translate(xy[0], xy[1])
756 self.stale = True
757
758 def get_offset(self):
759 """Return offset of the container."""
760 return self._offset
761
762 def get_bbox(self, renderer):
763 _, h_, d_ = renderer.get_text_width_height_descent(
764 "lp", self._text._fontproperties,
765 ismath="TeX" if self._text.get_usetex() else False)
766
767 bbox, info, yd = self._text._get_layout(renderer)
768 w, h = bbox.size
769
770 self._baseline_transform.clear()
771
772 if len(info) > 1 and self._multilinebaseline:
773 yd_new = 0.5 * h - 0.5 * (h_ - d_)
774 self._baseline_transform.translate(0, yd - yd_new)
775 yd = yd_new
776 else: # single line
777 h_d = max(h_ - d_, h - yd)
778 h = h_d + yd
779
780 ha = self._text.get_horizontalalignment()
781 x0 = {"left": 0, "center": -w / 2, "right": -w}[ha]
782
783 return Bbox.from_bounds(x0, -yd, w, h)
784
785 def draw(self, renderer):
786 # docstring inherited
787 self._text.draw(renderer)
788 _bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
789 self.stale = False
790
791
792class AuxTransformBox(OffsetBox):
793 """
794 Offset Box with the aux_transform. Its children will be
795 transformed with the aux_transform first then will be
796 offsetted. The absolute coordinate of the aux_transform is meaning
797 as it will be automatically adjust so that the left-lower corner
798 of the bounding box of children will be set to (0, 0) before the
799 offset transform.
800
801 It is similar to drawing area, except that the extent of the box
802 is not predetermined but calculated from the window extent of its
803 children. Furthermore, the extent of the children will be
804 calculated in the transformed coordinate.
805 """
806 def __init__(self, aux_transform):
807 self.aux_transform = aux_transform
808 super().__init__()
809 self.offset_transform = mtransforms.Affine2D()
810 # ref_offset_transform makes offset_transform always relative to the
811 # lower-left corner of the bbox of its children.
812 self.ref_offset_transform = mtransforms.Affine2D()
813
814 def add_artist(self, a):
815 """Add an `.Artist` to the container box."""
816 self._children.append(a)
817 a.set_transform(self.get_transform())
818 self.stale = True
819
820 def get_transform(self):
821 """
822 Return the :class:`~matplotlib.transforms.Transform` applied
823 to the children
824 """
825 return (self.aux_transform
826 + self.ref_offset_transform
827 + self.offset_transform)
828
829 def set_transform(self, t):
830 """
831 set_transform is ignored.
832 """
833
834 def set_offset(self, xy):
835 """
836 Set the offset of the container.
837
838 Parameters
839 ----------
840 xy : (float, float)
841 The (x, y) coordinates of the offset in display units.
842 """
843 self._offset = xy
844 self.offset_transform.clear()
845 self.offset_transform.translate(xy[0], xy[1])
846 self.stale = True
847
848 def get_offset(self):
849 """Return offset of the container."""
850 return self._offset
851
852 def get_bbox(self, renderer):
853 # clear the offset transforms
854 _off = self.offset_transform.get_matrix() # to be restored later
855 self.ref_offset_transform.clear()
856 self.offset_transform.clear()
857 # calculate the extent
858 bboxes = [c.get_window_extent(renderer) for c in self._children]
859 ub = Bbox.union(bboxes)
860 # adjust ref_offset_transform
861 self.ref_offset_transform.translate(-ub.x0, -ub.y0)
862 # restore offset transform
863 self.offset_transform.set_matrix(_off)
864 return Bbox.from_bounds(0, 0, ub.width, ub.height)
865
866 def draw(self, renderer):
867 # docstring inherited
868 for c in self._children:
869 c.draw(renderer)
870 _bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
871 self.stale = False
872
873
874class AnchoredOffsetbox(OffsetBox):
875 """
876 An offset box placed according to location *loc*.
877
878 AnchoredOffsetbox has a single child. When multiple children are needed,
879 use an extra OffsetBox to enclose them. By default, the offset box is
880 anchored against its parent Axes. You may explicitly specify the
881 *bbox_to_anchor*.
882 """
883 zorder = 5 # zorder of the legend
884
885 # Location codes
886 codes = {'upper right': 1,
887 'upper left': 2,
888 'lower left': 3,
889 'lower right': 4,
890 'right': 5,
891 'center left': 6,
892 'center right': 7,
893 'lower center': 8,
894 'upper center': 9,
895 'center': 10,
896 }
897
898 def __init__(self, loc, *,
899 pad=0.4, borderpad=0.5,
900 child=None, prop=None, frameon=True,
901 bbox_to_anchor=None,
902 bbox_transform=None,
903 **kwargs):
904 """
905 Parameters
906 ----------
907 loc : str
908 The box location. Valid locations are
909 'upper left', 'upper center', 'upper right',
910 'center left', 'center', 'center right',
911 'lower left', 'lower center', 'lower right'.
912 For backward compatibility, numeric values are accepted as well.
913 See the parameter *loc* of `.Legend` for details.
914 pad : float, default: 0.4
915 Padding around the child as fraction of the fontsize.
916 borderpad : float, default: 0.5
917 Padding between the offsetbox frame and the *bbox_to_anchor*.
918 child : `.OffsetBox`
919 The box that will be anchored.
920 prop : `.FontProperties`
921 This is only used as a reference for paddings. If not given,
922 :rc:`legend.fontsize` is used.
923 frameon : bool
924 Whether to draw a frame around the box.
925 bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats
926 Box that is used to position the legend in conjunction with *loc*.
927 bbox_transform : None or :class:`matplotlib.transforms.Transform`
928 The transform for the bounding box (*bbox_to_anchor*).
929 **kwargs
930 All other parameters are passed on to `.OffsetBox`.
931
932 Notes
933 -----
934 See `.Legend` for a detailed description of the anchoring mechanism.
935 """
936 super().__init__(**kwargs)
937
938 self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform)
939 self.set_child(child)
940
941 if isinstance(loc, str):
942 loc = _api.check_getitem(self.codes, loc=loc)
943
944 self.loc = loc
945 self.borderpad = borderpad
946 self.pad = pad
947
948 if prop is None:
949 self.prop = FontProperties(size=mpl.rcParams["legend.fontsize"])
950 else:
951 self.prop = FontProperties._from_any(prop)
952 if isinstance(prop, dict) and "size" not in prop:
953 self.prop.set_size(mpl.rcParams["legend.fontsize"])
954
955 self.patch = FancyBboxPatch(
956 xy=(0.0, 0.0), width=1., height=1.,
957 facecolor='w', edgecolor='k',
958 mutation_scale=self.prop.get_size_in_points(),
959 snap=True,
960 visible=frameon,
961 boxstyle="square,pad=0",
962 )
963
964 def set_child(self, child):
965 """Set the child to be anchored."""
966 self._child = child
967 if child is not None:
968 child.axes = self.axes
969 self.stale = True
970
971 def get_child(self):
972 """Return the child."""
973 return self._child
974
975 def get_children(self):
976 """Return the list of children."""
977 return [self._child]
978
979 def get_bbox(self, renderer):
980 # docstring inherited
981 fontsize = renderer.points_to_pixels(self.prop.get_size_in_points())
982 pad = self.pad * fontsize
983 return self.get_child().get_bbox(renderer).padded(pad)
984
985 def get_bbox_to_anchor(self):
986 """Return the bbox that the box is anchored to."""
987 if self._bbox_to_anchor is None:
988 return self.axes.bbox
989 else:
990 transform = self._bbox_to_anchor_transform
991 if transform is None:
992 return self._bbox_to_anchor
993 else:
994 return TransformedBbox(self._bbox_to_anchor, transform)
995
996 def set_bbox_to_anchor(self, bbox, transform=None):
997 """
998 Set the bbox that the box is anchored to.
999
1000 *bbox* can be a Bbox instance, a list of [left, bottom, width,
1001 height], or a list of [left, bottom] where the width and
1002 height will be assumed to be zero. The bbox will be
1003 transformed to display coordinate by the given transform.
1004 """
1005 if bbox is None or isinstance(bbox, BboxBase):
1006 self._bbox_to_anchor = bbox
1007 else:
1008 try:
1009 l = len(bbox)
1010 except TypeError as err:
1011 raise ValueError(f"Invalid bbox: {bbox}") from err
1012
1013 if l == 2:
1014 bbox = [bbox[0], bbox[1], 0, 0]
1015
1016 self._bbox_to_anchor = Bbox.from_bounds(*bbox)
1017
1018 self._bbox_to_anchor_transform = transform
1019 self.stale = True
1020
1021 @_compat_get_offset
1022 def get_offset(self, bbox, renderer):
1023 # docstring inherited
1024 pad = (self.borderpad
1025 * renderer.points_to_pixels(self.prop.get_size_in_points()))
1026 bbox_to_anchor = self.get_bbox_to_anchor()
1027 x0, y0 = _get_anchored_bbox(
1028 self.loc, Bbox.from_bounds(0, 0, bbox.width, bbox.height),
1029 bbox_to_anchor, pad)
1030 return x0 - bbox.x0, y0 - bbox.y0
1031
1032 def update_frame(self, bbox, fontsize=None):
1033 self.patch.set_bounds(bbox.bounds)
1034 if fontsize:
1035 self.patch.set_mutation_scale(fontsize)
1036
1037 def draw(self, renderer):
1038 # docstring inherited
1039 if not self.get_visible():
1040 return
1041
1042 # update the location and size of the legend
1043 bbox = self.get_window_extent(renderer)
1044 fontsize = renderer.points_to_pixels(self.prop.get_size_in_points())
1045 self.update_frame(bbox, fontsize)
1046 self.patch.draw(renderer)
1047
1048 px, py = self.get_offset(self.get_bbox(renderer), renderer)
1049 self.get_child().set_offset((px, py))
1050 self.get_child().draw(renderer)
1051 self.stale = False
1052
1053
1054def _get_anchored_bbox(loc, bbox, parentbbox, borderpad):
1055 """
1056 Return the (x, y) position of the *bbox* anchored at the *parentbbox* with
1057 the *loc* code with the *borderpad*.
1058 """
1059 # This is only called internally and *loc* should already have been
1060 # validated. If 0 (None), we just let ``bbox.anchored`` raise.
1061 c = [None, "NE", "NW", "SW", "SE", "E", "W", "E", "S", "N", "C"][loc]
1062 container = parentbbox.padded(-borderpad)
1063 return bbox.anchored(c, container=container).p0
1064
1065
1066class AnchoredText(AnchoredOffsetbox):
1067 """
1068 AnchoredOffsetbox with Text.
1069 """
1070
1071 def __init__(self, s, loc, *, pad=0.4, borderpad=0.5, prop=None, **kwargs):
1072 """
1073 Parameters
1074 ----------
1075 s : str
1076 Text.
1077
1078 loc : str
1079 Location code. See `AnchoredOffsetbox`.
1080
1081 pad : float, default: 0.4
1082 Padding around the text as fraction of the fontsize.
1083
1084 borderpad : float, default: 0.5
1085 Spacing between the offsetbox frame and the *bbox_to_anchor*.
1086
1087 prop : dict, optional
1088 Dictionary of keyword parameters to be passed to the
1089 `~matplotlib.text.Text` instance contained inside AnchoredText.
1090
1091 **kwargs
1092 All other parameters are passed to `AnchoredOffsetbox`.
1093 """
1094
1095 if prop is None:
1096 prop = {}
1097 badkwargs = {'va', 'verticalalignment'}
1098 if badkwargs & set(prop):
1099 raise ValueError(
1100 'Mixing verticalalignment with AnchoredText is not supported.')
1101
1102 self.txt = TextArea(s, textprops=prop)
1103 fp = self.txt._text.get_fontproperties()
1104 super().__init__(
1105 loc, pad=pad, borderpad=borderpad, child=self.txt, prop=fp,
1106 **kwargs)
1107
1108
1109class OffsetImage(OffsetBox):
1110
1111 def __init__(self, arr, *,
1112 zoom=1,
1113 cmap=None,
1114 norm=None,
1115 interpolation=None,
1116 origin=None,
1117 filternorm=True,
1118 filterrad=4.0,
1119 resample=False,
1120 dpi_cor=True,
1121 **kwargs
1122 ):
1123
1124 super().__init__()
1125 self._dpi_cor = dpi_cor
1126
1127 self.image = BboxImage(bbox=self.get_window_extent,
1128 cmap=cmap,
1129 norm=norm,
1130 interpolation=interpolation,
1131 origin=origin,
1132 filternorm=filternorm,
1133 filterrad=filterrad,
1134 resample=resample,
1135 **kwargs
1136 )
1137
1138 self._children = [self.image]
1139
1140 self.set_zoom(zoom)
1141 self.set_data(arr)
1142
1143 def set_data(self, arr):
1144 self._data = np.asarray(arr)
1145 self.image.set_data(self._data)
1146 self.stale = True
1147
1148 def get_data(self):
1149 return self._data
1150
1151 def set_zoom(self, zoom):
1152 self._zoom = zoom
1153 self.stale = True
1154
1155 def get_zoom(self):
1156 return self._zoom
1157
1158 def get_offset(self):
1159 """Return offset of the container."""
1160 return self._offset
1161
1162 def get_children(self):
1163 return [self.image]
1164
1165 def get_bbox(self, renderer):
1166 dpi_cor = renderer.points_to_pixels(1.) if self._dpi_cor else 1.
1167 zoom = self.get_zoom()
1168 data = self.get_data()
1169 ny, nx = data.shape[:2]
1170 w, h = dpi_cor * nx * zoom, dpi_cor * ny * zoom
1171 return Bbox.from_bounds(0, 0, w, h)
1172
1173 def draw(self, renderer):
1174 # docstring inherited
1175 self.image.draw(renderer)
1176 # bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
1177 self.stale = False
1178
1179
1180class AnnotationBbox(martist.Artist, mtext._AnnotationBase):
1181 """
1182 Container for an `OffsetBox` referring to a specific position *xy*.
1183
1184 Optionally an arrow pointing from the offsetbox to *xy* can be drawn.
1185
1186 This is like `.Annotation`, but with `OffsetBox` instead of `.Text`.
1187 """
1188
1189 zorder = 3
1190
1191 def __str__(self):
1192 return f"AnnotationBbox({self.xy[0]:g},{self.xy[1]:g})"
1193
1194 @_docstring.dedent_interpd
1195 def __init__(self, offsetbox, xy, xybox=None, xycoords='data', boxcoords=None, *,
1196 frameon=True, pad=0.4, # FancyBboxPatch boxstyle.
1197 annotation_clip=None,
1198 box_alignment=(0.5, 0.5),
1199 bboxprops=None,
1200 arrowprops=None,
1201 fontsize=None,
1202 **kwargs):
1203 """
1204 Parameters
1205 ----------
1206 offsetbox : `OffsetBox`
1207
1208 xy : (float, float)
1209 The point *(x, y)* to annotate. The coordinate system is determined
1210 by *xycoords*.
1211
1212 xybox : (float, float), default: *xy*
1213 The position *(x, y)* to place the text at. The coordinate system
1214 is determined by *boxcoords*.
1215
1216 xycoords : single or two-tuple of str or `.Artist` or `.Transform` or \
1217callable, default: 'data'
1218 The coordinate system that *xy* is given in. See the parameter
1219 *xycoords* in `.Annotation` for a detailed description.
1220
1221 boxcoords : single or two-tuple of str or `.Artist` or `.Transform` \
1222or callable, default: value of *xycoords*
1223 The coordinate system that *xybox* is given in. See the parameter
1224 *textcoords* in `.Annotation` for a detailed description.
1225
1226 frameon : bool, default: True
1227 By default, the text is surrounded by a white `.FancyBboxPatch`
1228 (accessible as the ``patch`` attribute of the `.AnnotationBbox`).
1229 If *frameon* is set to False, this patch is made invisible.
1230
1231 annotation_clip: bool or None, default: None
1232 Whether to clip (i.e. not draw) the annotation when the annotation
1233 point *xy* is outside the Axes area.
1234
1235 - If *True*, the annotation will be clipped when *xy* is outside
1236 the Axes.
1237 - If *False*, the annotation will always be drawn.
1238 - If *None*, the annotation will be clipped when *xy* is outside
1239 the Axes and *xycoords* is 'data'.
1240
1241 pad : float, default: 0.4
1242 Padding around the offsetbox.
1243
1244 box_alignment : (float, float)
1245 A tuple of two floats for a vertical and horizontal alignment of
1246 the offset box w.r.t. the *boxcoords*.
1247 The lower-left corner is (0, 0) and upper-right corner is (1, 1).
1248
1249 bboxprops : dict, optional
1250 A dictionary of properties to set for the annotation bounding box,
1251 for example *boxstyle* and *alpha*. See `.FancyBboxPatch` for
1252 details.
1253
1254 arrowprops: dict, optional
1255 Arrow properties, see `.Annotation` for description.
1256
1257 fontsize: float or str, optional
1258 Translated to points and passed as *mutation_scale* into
1259 `.FancyBboxPatch` to scale attributes of the box style (e.g. pad
1260 or rounding_size). The name is chosen in analogy to `.Text` where
1261 *fontsize* defines the mutation scale as well. If not given,
1262 :rc:`legend.fontsize` is used. See `.Text.set_fontsize` for valid
1263 values.
1264
1265 **kwargs
1266 Other `AnnotationBbox` properties. See `.AnnotationBbox.set` for
1267 a list.
1268 """
1269
1270 martist.Artist.__init__(self)
1271 mtext._AnnotationBase.__init__(
1272 self, xy, xycoords=xycoords, annotation_clip=annotation_clip)
1273
1274 self.offsetbox = offsetbox
1275 self.arrowprops = arrowprops.copy() if arrowprops is not None else None
1276 self.set_fontsize(fontsize)
1277 self.xybox = xybox if xybox is not None else xy
1278 self.boxcoords = boxcoords if boxcoords is not None else xycoords
1279 self._box_alignment = box_alignment
1280
1281 if arrowprops is not None:
1282 self._arrow_relpos = self.arrowprops.pop("relpos", (0.5, 0.5))
1283 self.arrow_patch = FancyArrowPatch((0, 0), (1, 1),
1284 **self.arrowprops)
1285 else:
1286 self._arrow_relpos = None
1287 self.arrow_patch = None
1288
1289 self.patch = FancyBboxPatch( # frame
1290 xy=(0.0, 0.0), width=1., height=1.,
1291 facecolor='w', edgecolor='k',
1292 mutation_scale=self.prop.get_size_in_points(),
1293 snap=True,
1294 visible=frameon,
1295 )
1296 self.patch.set_boxstyle("square", pad=pad)
1297 if bboxprops:
1298 self.patch.set(**bboxprops)
1299
1300 self._internal_update(kwargs)
1301
1302 @property
1303 def xyann(self):
1304 return self.xybox
1305
1306 @xyann.setter
1307 def xyann(self, xyann):
1308 self.xybox = xyann
1309 self.stale = True
1310
1311 @property
1312 def anncoords(self):
1313 return self.boxcoords
1314
1315 @anncoords.setter
1316 def anncoords(self, coords):
1317 self.boxcoords = coords
1318 self.stale = True
1319
1320 def contains(self, mouseevent):
1321 if self._different_canvas(mouseevent):
1322 return False, {}
1323 if not self._check_xy(None):
1324 return False, {}
1325 return self.offsetbox.contains(mouseevent)
1326 # self.arrow_patch is currently not checked as this can be a line - JJ
1327
1328 def get_children(self):
1329 children = [self.offsetbox, self.patch]
1330 if self.arrow_patch:
1331 children.append(self.arrow_patch)
1332 return children
1333
1334 def set_figure(self, fig):
1335 if self.arrow_patch is not None:
1336 self.arrow_patch.set_figure(fig)
1337 self.offsetbox.set_figure(fig)
1338 martist.Artist.set_figure(self, fig)
1339
1340 def set_fontsize(self, s=None):
1341 """
1342 Set the fontsize in points.
1343
1344 If *s* is not given, reset to :rc:`legend.fontsize`.
1345 """
1346 if s is None:
1347 s = mpl.rcParams["legend.fontsize"]
1348
1349 self.prop = FontProperties(size=s)
1350 self.stale = True
1351
1352 def get_fontsize(self):
1353 """Return the fontsize in points."""
1354 return self.prop.get_size_in_points()
1355
1356 def get_window_extent(self, renderer=None):
1357 # docstring inherited
1358 if renderer is None:
1359 renderer = self.figure._get_renderer()
1360 self.update_positions(renderer)
1361 return Bbox.union([child.get_window_extent(renderer)
1362 for child in self.get_children()])
1363
1364 def get_tightbbox(self, renderer=None):
1365 # docstring inherited
1366 if renderer is None:
1367 renderer = self.figure._get_renderer()
1368 self.update_positions(renderer)
1369 return Bbox.union([child.get_tightbbox(renderer)
1370 for child in self.get_children()])
1371
1372 def update_positions(self, renderer):
1373 """Update pixel positions for the annotated point, the text, and the arrow."""
1374
1375 ox0, oy0 = self._get_xy(renderer, self.xybox, self.boxcoords)
1376 bbox = self.offsetbox.get_bbox(renderer)
1377 fw, fh = self._box_alignment
1378 self.offsetbox.set_offset(
1379 (ox0 - fw*bbox.width - bbox.x0, oy0 - fh*bbox.height - bbox.y0))
1380
1381 bbox = self.offsetbox.get_window_extent(renderer)
1382 self.patch.set_bounds(bbox.bounds)
1383
1384 mutation_scale = renderer.points_to_pixels(self.get_fontsize())
1385 self.patch.set_mutation_scale(mutation_scale)
1386
1387 if self.arrowprops:
1388 # Use FancyArrowPatch if self.arrowprops has "arrowstyle" key.
1389
1390 # Adjust the starting point of the arrow relative to the textbox.
1391 # TODO: Rotation needs to be accounted.
1392 arrow_begin = bbox.p0 + bbox.size * self._arrow_relpos
1393 arrow_end = self._get_position_xy(renderer)
1394 # The arrow (from arrow_begin to arrow_end) will be first clipped
1395 # by patchA and patchB, then shrunk by shrinkA and shrinkB (in
1396 # points). If patch A is not set, self.bbox_patch is used.
1397 self.arrow_patch.set_positions(arrow_begin, arrow_end)
1398
1399 if "mutation_scale" in self.arrowprops:
1400 mutation_scale = renderer.points_to_pixels(
1401 self.arrowprops["mutation_scale"])
1402 # Else, use fontsize-based mutation_scale defined above.
1403 self.arrow_patch.set_mutation_scale(mutation_scale)
1404
1405 patchA = self.arrowprops.get("patchA", self.patch)
1406 self.arrow_patch.set_patchA(patchA)
1407
1408 def draw(self, renderer):
1409 # docstring inherited
1410 if not self.get_visible() or not self._check_xy(renderer):
1411 return
1412 renderer.open_group(self.__class__.__name__, gid=self.get_gid())
1413 self.update_positions(renderer)
1414 if self.arrow_patch is not None:
1415 if self.arrow_patch.figure is None and self.figure is not None:
1416 self.arrow_patch.figure = self.figure
1417 self.arrow_patch.draw(renderer)
1418 self.patch.draw(renderer)
1419 self.offsetbox.draw(renderer)
1420 renderer.close_group(self.__class__.__name__)
1421 self.stale = False
1422
1423
1424class DraggableBase:
1425 """
1426 Helper base class for a draggable artist (legend, offsetbox).
1427
1428 Derived classes must override the following methods::
1429
1430 def save_offset(self):
1431 '''
1432 Called when the object is picked for dragging; should save the
1433 reference position of the artist.
1434 '''
1435
1436 def update_offset(self, dx, dy):
1437 '''
1438 Called during the dragging; (*dx*, *dy*) is the pixel offset from
1439 the point where the mouse drag started.
1440 '''
1441
1442 Optionally, you may override the following method::
1443
1444 def finalize_offset(self):
1445 '''Called when the mouse is released.'''
1446
1447 In the current implementation of `.DraggableLegend` and
1448 `DraggableAnnotation`, `update_offset` places the artists in display
1449 coordinates, and `finalize_offset` recalculates their position in axes
1450 coordinate and set a relevant attribute.
1451 """
1452
1453 def __init__(self, ref_artist, use_blit=False):
1454 self.ref_artist = ref_artist
1455 if not ref_artist.pickable():
1456 ref_artist.set_picker(True)
1457 self.got_artist = False
1458 self._use_blit = use_blit and self.canvas.supports_blit
1459 callbacks = self.canvas.callbacks
1460 self._disconnectors = [
1461 functools.partial(
1462 callbacks.disconnect, callbacks._connect_picklable(name, func))
1463 for name, func in [
1464 ("pick_event", self.on_pick),
1465 ("button_release_event", self.on_release),
1466 ("motion_notify_event", self.on_motion),
1467 ]
1468 ]
1469
1470 # A property, not an attribute, to maintain picklability.
1471 canvas = property(lambda self: self.ref_artist.figure.canvas)
1472 cids = property(lambda self: [
1473 disconnect.args[0] for disconnect in self._disconnectors[:2]])
1474
1475 def on_motion(self, evt):
1476 if self._check_still_parented() and self.got_artist:
1477 dx = evt.x - self.mouse_x
1478 dy = evt.y - self.mouse_y
1479 self.update_offset(dx, dy)
1480 if self._use_blit:
1481 self.canvas.restore_region(self.background)
1482 self.ref_artist.draw(
1483 self.ref_artist.figure._get_renderer())
1484 self.canvas.blit()
1485 else:
1486 self.canvas.draw()
1487
1488 def on_pick(self, evt):
1489 if self._check_still_parented():
1490 if evt.artist == self.ref_artist:
1491 self.mouse_x = evt.mouseevent.x
1492 self.mouse_y = evt.mouseevent.y
1493 self.save_offset()
1494 self.got_artist = True
1495 if self.got_artist and self._use_blit:
1496 self.ref_artist.set_animated(True)
1497 self.canvas.draw()
1498 self.background = \
1499 self.canvas.copy_from_bbox(self.ref_artist.figure.bbox)
1500 self.ref_artist.draw(
1501 self.ref_artist.figure._get_renderer())
1502 self.canvas.blit()
1503
1504 def on_release(self, event):
1505 if self._check_still_parented() and self.got_artist:
1506 self.finalize_offset()
1507 self.got_artist = False
1508 if self._use_blit:
1509 self.canvas.restore_region(self.background)
1510 self.ref_artist.draw(self.ref_artist.figure._get_renderer())
1511 self.canvas.blit()
1512 self.ref_artist.set_animated(False)
1513
1514 def _check_still_parented(self):
1515 if self.ref_artist.figure is None:
1516 self.disconnect()
1517 return False
1518 else:
1519 return True
1520
1521 def disconnect(self):
1522 """Disconnect the callbacks."""
1523 for disconnector in self._disconnectors:
1524 disconnector()
1525
1526 def save_offset(self):
1527 pass
1528
1529 def update_offset(self, dx, dy):
1530 pass
1531
1532 def finalize_offset(self):
1533 pass
1534
1535
1536class DraggableOffsetBox(DraggableBase):
1537 def __init__(self, ref_artist, offsetbox, use_blit=False):
1538 super().__init__(ref_artist, use_blit=use_blit)
1539 self.offsetbox = offsetbox
1540
1541 def save_offset(self):
1542 offsetbox = self.offsetbox
1543 renderer = offsetbox.figure._get_renderer()
1544 offset = offsetbox.get_offset(offsetbox.get_bbox(renderer), renderer)
1545 self.offsetbox_x, self.offsetbox_y = offset
1546 self.offsetbox.set_offset(offset)
1547
1548 def update_offset(self, dx, dy):
1549 loc_in_canvas = self.offsetbox_x + dx, self.offsetbox_y + dy
1550 self.offsetbox.set_offset(loc_in_canvas)
1551
1552 def get_loc_in_canvas(self):
1553 offsetbox = self.offsetbox
1554 renderer = offsetbox.figure._get_renderer()
1555 bbox = offsetbox.get_bbox(renderer)
1556 ox, oy = offsetbox._offset
1557 loc_in_canvas = (ox + bbox.x0, oy + bbox.y0)
1558 return loc_in_canvas
1559
1560
1561class DraggableAnnotation(DraggableBase):
1562 def __init__(self, annotation, use_blit=False):
1563 super().__init__(annotation, use_blit=use_blit)
1564 self.annotation = annotation
1565
1566 def save_offset(self):
1567 ann = self.annotation
1568 self.ox, self.oy = ann.get_transform().transform(ann.xyann)
1569
1570 def update_offset(self, dx, dy):
1571 ann = self.annotation
1572 ann.xyann = ann.get_transform().inverted().transform(
1573 (self.ox + dx, self.oy + dy))