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