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

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2The `.OffsetBox` is a simple container artist. Its child artists are
3meant to be drawn at a relative position to OffsetBox. The [VH]Packer,
4DrawingArea and TextArea are derived from the OffsetBox.
6The [VH]Packer automatically adjust the relative positions of their
7children, which should be instances of the OffsetBox. This is used to
8align similar artists together, e.g., in legend.
10The DrawingArea can contain any Artist as a child. The
11DrawingArea has a fixed width and height. The position of children
12relative to the parent is fixed. The TextArea is contains a single
13Text instance. The width and height of the TextArea instance is the
14width and height of the its child text.
15"""
17import numpy as np
19from matplotlib import cbook, docstring, rcParams
20import matplotlib.artist as martist
21import matplotlib.path as mpath
22import matplotlib.text as mtext
23import matplotlib.transforms as mtransforms
24from matplotlib.font_manager import FontProperties
25from matplotlib.image import BboxImage
26from matplotlib.patches import (
27 FancyBboxPatch, FancyArrowPatch, bbox_artist as mbbox_artist)
28from matplotlib.text import _AnnotationBase
29from matplotlib.transforms import Bbox, BboxBase, TransformedBbox
32DEBUG = False
35# for debugging use
36def bbox_artist(*args, **kwargs):
37 if DEBUG:
38 mbbox_artist(*args, **kwargs)
40# _get_packed_offsets() and _get_aligned_offsets() are coded assuming
41# that we are packing boxes horizontally. But same function will be
42# used with vertical packing.
45def _get_packed_offsets(wd_list, total, sep, mode="fixed"):
46 """
47 Given a list of (width, xdescent) of each boxes, calculate the
48 total width and the x-offset positions of each items according to
49 *mode*. xdescent is analogous to the usual descent, but along the
50 x-direction. xdescent values are currently ignored.
52 For simplicity of the description, the terminology used here assumes a
53 horizontal layout, but the function works equally for a vertical layout.
55 There are three packing modes:
57 - 'fixed': The elements are packed tight to the left with a spacing of
58 *sep* in between. If *total* is *None* the returned total will be the
59 right edge of the last box. A non-*None* total will be passed unchecked
60 to the output. In particular this means that right edge of the last
61 box may be further to the right than the returned total.
63 - 'expand': Distribute the boxes with equal spacing so that the left edge
64 of the first box is at 0, and the right edge of the last box is at
65 *total*. The parameter *sep* is ignored in this mode. A total of *None*
66 is accepted and considered equal to 1. The total is returned unchanged
67 (except for the conversion *None* to 1). If the total is smaller than
68 the sum of the widths, the laid out boxes will overlap.
70 - 'equal': If *total* is given, the total space is divided in N equal
71 ranges and each box is left-aligned within its subspace.
72 Otherwise (*total* is *None*), *sep* must be provided and each box is
73 left-aligned in its subspace of width ``(max(widths) + sep)``. The
74 total width is then calculated to be ``N * (max(widths) + sep)``.
76 Parameters
77 ----------
78 wd_list : list of (float, float)
79 (width, xdescent) of boxes to be packed.
80 total : float or None
81 Intended total length. *None* if not used.
82 sep : float
83 Spacing between boxes.
84 mode : {'fixed', 'expand', 'equal'}
85 The packing mode.
87 Returns
88 -------
89 total : float
90 The total width needed to accommodate the laid out boxes.
91 offsets : array of float
92 The left offsets of the boxes.
93 """
94 w_list, d_list = zip(*wd_list)
95 # d_list is currently not used.
97 if mode == "fixed":
98 offsets_ = np.cumsum([0] + [w + sep for w in w_list])
99 offsets = offsets_[:-1]
100 if total is None:
101 total = offsets_[-1] - sep
102 return total, offsets
104 elif mode == "expand":
105 # This is a bit of a hack to avoid a TypeError when *total*
106 # is None and used in conjugation with tight layout.
107 if total is None:
108 total = 1
109 if len(w_list) > 1:
110 sep = (total - sum(w_list)) / (len(w_list) - 1)
111 else:
112 sep = 0
113 offsets_ = np.cumsum([0] + [w + sep for w in w_list])
114 offsets = offsets_[:-1]
115 return total, offsets
117 elif mode == "equal":
118 maxh = max(w_list)
119 if total is None:
120 if sep is None:
121 raise ValueError("total and sep cannot both be None when "
122 "using layout mode 'equal'.")
123 total = (maxh + sep) * len(w_list)
124 else:
125 sep = total / len(w_list) - maxh
126 offsets = (maxh + sep) * np.arange(len(w_list))
127 return total, offsets
129 else:
130 raise ValueError("Unknown mode : %s" % (mode,))
133def _get_aligned_offsets(hd_list, height, align="baseline"):
134 """
135 Given a list of (height, descent) of each boxes, align the boxes
136 with *align* and calculate the y-offsets of each boxes.
137 total width and the offset positions of each items according to
138 *mode*. xdescent is analogous to the usual descent, but along the
139 x-direction. xdescent values are currently ignored.
141 *hd_list* : list of (width, xdescent) of boxes to be aligned.
142 *sep* : spacing between boxes
143 *height* : Intended total length. None if not used.
144 *align* : align mode. 'baseline', 'top', 'bottom', or 'center'.
145 """
147 if height is None:
148 height = max(h for h, d in hd_list)
150 if align == "baseline":
151 height_descent = max(h - d for h, d in hd_list)
152 descent = max(d for h, d in hd_list)
153 height = height_descent + descent
154 offsets = [0. for h, d in hd_list]
155 elif align in ["left", "top"]:
156 descent = 0.
157 offsets = [d for h, d in hd_list]
158 elif align in ["right", "bottom"]:
159 descent = 0.
160 offsets = [height - h + d for h, d in hd_list]
161 elif align == "center":
162 descent = 0.
163 offsets = [(height - h) * .5 + d for h, d in hd_list]
164 else:
165 raise ValueError("Unknown Align mode : %s" % (align,))
167 return height, descent, offsets
170class OffsetBox(martist.Artist):
171 """
172 The OffsetBox is a simple container artist. The child artist are meant
173 to be drawn at a relative position to its parent.
174 """
175 def __init__(self, *args, **kwargs):
177 super().__init__(*args, **kwargs)
179 # Clipping has not been implemented in the OffesetBox family, so
180 # disable the clip flag for consistency. It can always be turned back
181 # on to zero effect.
182 self.set_clip_on(False)
184 self._children = []
185 self._offset = (0, 0)
187 def set_figure(self, fig):
188 """
189 Set the `.Figure` for the `.OffsetBox` and all its children.
191 Parameters
192 ----------
193 fig : `~matplotlib.figure.Figure`
194 """
195 martist.Artist.set_figure(self, fig)
196 for c in self.get_children():
197 c.set_figure(fig)
199 @martist.Artist.axes.setter
200 def axes(self, ax):
201 # TODO deal with this better
202 martist.Artist.axes.fset(self, ax)
203 for c in self.get_children():
204 if c is not None:
205 c.axes = ax
207 def contains(self, mouseevent):
208 """
209 Delegate the mouse event contains-check to the children.
211 As a container, the `.OffsetBox` does not respond itself to
212 mouseevents.
214 Parameters
215 ----------
216 mouseevent : `matplotlib.backend_bases.MouseEvent`
218 Returns
219 -------
220 contains : bool
221 Whether any values are within the radius.
222 details : dict
223 An artist-specific dictionary of details of the event context,
224 such as which points are contained in the pick radius. See the
225 individual Artist subclasses for details.
227 See Also
228 --------
229 .Artist.contains
230 """
231 inside, info = self._default_contains(mouseevent)
232 if inside is not None:
233 return inside, info
234 for c in self.get_children():
235 a, b = c.contains(mouseevent)
236 if a:
237 return a, b
238 return False, {}
240 def set_offset(self, xy):
241 """
242 Set the offset.
244 Parameters
245 ----------
246 xy : (float, float) or callable
247 The (x, y) coordinates of the offset in display units. These can
248 either be given explicitly as a tuple (x, y), or by providing a
249 function that converts the extent into the offset. This function
250 must have the signature::
252 def offset(width, height, xdescent, ydescent, renderer) \
253-> (float, float)
254 """
255 self._offset = xy
256 self.stale = True
258 def get_offset(self, width, height, xdescent, ydescent, renderer):
259 """
260 Return the offset as a tuple (x, y).
262 The extent parameters have to be provided to handle the case where the
263 offset is dynamically determined by a callable (see
264 `~.OffsetBox.set_offset`).
266 Parameters
267 ----------
268 width, height, xdescent, ydescent
269 Extent parameters.
270 renderer : `.RendererBase` subclass
272 """
273 return (self._offset(width, height, xdescent, ydescent, renderer)
274 if callable(self._offset)
275 else self._offset)
277 def set_width(self, width):
278 """
279 Set the width of the box.
281 Parameters
282 ----------
283 width : float
284 """
285 self.width = width
286 self.stale = True
288 def set_height(self, height):
289 """
290 Set the height of the box.
292 Parameters
293 ----------
294 height : float
295 """
296 self.height = height
297 self.stale = True
299 def get_visible_children(self):
300 r"""Return a list of the visible child `.Artist`\s."""
301 return [c for c in self._children if c.get_visible()]
303 def get_children(self):
304 r"""Return a list of the child `.Artist`\s."""
305 return self._children
307 def get_extent_offsets(self, renderer):
308 """
309 Update offset of the children and return the extent of the box.
311 Parameters
312 ----------
313 renderer : `.RendererBase` subclass
315 Returns
316 -------
317 width
318 height
319 xdescent
320 ydescent
321 list of (xoffset, yoffset) pairs
322 """
323 raise NotImplementedError(
324 "get_extent_offsets must be overridden in derived classes.")
326 def get_extent(self, renderer):
327 """Return a tuple ``width, height, xdescent, ydescent`` of the box."""
328 w, h, xd, yd, offsets = self.get_extent_offsets(renderer)
329 return w, h, xd, yd
331 def get_window_extent(self, renderer):
332 """Return the bounding box (`.Bbox`) in display space."""
333 w, h, xd, yd, offsets = self.get_extent_offsets(renderer)
334 px, py = self.get_offset(w, h, xd, yd, renderer)
335 return mtransforms.Bbox.from_bounds(px - xd, py - yd, w, h)
337 def draw(self, renderer):
338 """
339 Update the location of children if necessary and draw them
340 to the given *renderer*.
341 """
342 width, height, xdescent, ydescent, offsets = self.get_extent_offsets(
343 renderer)
345 px, py = self.get_offset(width, height, xdescent, ydescent, renderer)
347 for c, (ox, oy) in zip(self.get_visible_children(), offsets):
348 c.set_offset((px + ox, py + oy))
349 c.draw(renderer)
351 bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
352 self.stale = False
355class PackerBase(OffsetBox):
356 def __init__(self, pad=None, sep=None, width=None, height=None,
357 align=None, mode=None,
358 children=None):
359 """
360 Parameters
361 ----------
362 pad : float, optional
363 The boundary padding in points.
365 sep : float, optional
366 The spacing between items in points.
368 width, height : float, optional
369 Width and height of the container box in pixels, calculated if
370 *None*.
372 align : {'top', 'bottom', 'left', 'right', 'center', 'baseline'}
373 Alignment of boxes.
375 mode : {'fixed', 'expand', 'equal'}
376 The packing mode.
378 - 'fixed' packs the given `.Artists` tight with *sep* spacing.
379 - 'expand' uses the maximal available space to distribute the
380 artists with equal spacing in between.
381 - 'equal': Each artist an equal fraction of the available space
382 and is left-aligned (or top-aligned) therein.
384 children : list of `.Artist`
385 The artists to pack.
387 Notes
388 -----
389 *pad* and *sep* need to given in points and will be scale with
390 the renderer dpi, while *width* and *height* need to be in
391 pixels.
392 """
393 super().__init__()
395 self.height = height
396 self.width = width
397 self.sep = sep
398 self.pad = pad
399 self.mode = mode
400 self.align = align
402 self._children = children
405class VPacker(PackerBase):
406 """
407 The VPacker has its children packed vertically. It automatically
408 adjust the relative positions of children in the drawing time.
409 """
410 def __init__(self, pad=None, sep=None, width=None, height=None,
411 align="baseline", mode="fixed",
412 children=None):
413 """
414 Parameters
415 ----------
416 pad : float, optional
417 The boundary padding in points.
419 sep : float, optional
420 The spacing between items in points.
422 width, height : float, optional
423 Width and height of the container box in pixels, calculated if
424 *None*.
426 align : {'top', 'bottom', 'left', 'right', 'center', 'baseline'}
427 Alignment of boxes.
429 mode : {'fixed', 'expand', 'equal'}
430 The packing mode.
432 - 'fixed' packs the given `.Artists` tight with *sep* spacing.
433 - 'expand' uses the maximal available space to distribute the
434 artists with equal spacing in between.
435 - 'equal': Each artist an equal fraction of the available space
436 and is left-aligned (or top-aligned) therein.
438 children : list of `.Artist`
439 The artists to pack.
441 Notes
442 -----
443 *pad* and *sep* need to given in points and will be scale with
444 the renderer dpi, while *width* and *height* need to be in
445 pixels.
446 """
447 super().__init__(pad, sep, width, height, align, mode, children)
449 def get_extent_offsets(self, renderer):
450 # docstring inherited
451 dpicor = renderer.points_to_pixels(1.)
452 pad = self.pad * dpicor
453 sep = self.sep * dpicor
455 if self.width is not None:
456 for c in self.get_visible_children():
457 if isinstance(c, PackerBase) and c.mode == "expand":
458 c.set_width(self.width)
460 whd_list = [c.get_extent(renderer)
461 for c in self.get_visible_children()]
462 whd_list = [(w, h, xd, (h - yd)) for w, h, xd, yd in whd_list]
464 wd_list = [(w, xd) for w, h, xd, yd in whd_list]
465 width, xdescent, xoffsets = _get_aligned_offsets(wd_list,
466 self.width,
467 self.align)
469 pack_list = [(h, yd) for w, h, xd, yd in whd_list]
470 height, yoffsets_ = _get_packed_offsets(pack_list, self.height,
471 sep, self.mode)
473 yoffsets = yoffsets_ + [yd for w, h, xd, yd in whd_list]
474 ydescent = height - yoffsets[0]
475 yoffsets = height - yoffsets
477 yoffsets = yoffsets - ydescent
479 return (width + 2 * pad, height + 2 * pad,
480 xdescent + pad, ydescent + pad,
481 list(zip(xoffsets, yoffsets)))
484class HPacker(PackerBase):
485 """
486 The HPacker has its children packed horizontally. It automatically
487 adjusts the relative positions of children at draw time.
488 """
489 def __init__(self, pad=None, sep=None, width=None, height=None,
490 align="baseline", mode="fixed",
491 children=None):
492 """
493 Parameters
494 ----------
495 pad : float, optional
496 The boundary padding in points.
498 sep : float, optional
499 The spacing between items in points.
501 width, height : float, optional
502 Width and height of the container box in pixels, calculated if
503 *None*.
505 align : {'top', 'bottom', 'left', 'right', 'center', 'baseline'}
506 Alignment of boxes.
508 mode : {'fixed', 'expand', 'equal'}
509 The packing mode.
511 - 'fixed' packs the given `.Artists` tight with *sep* spacing.
512 - 'expand' uses the maximal available space to distribute the
513 artists with equal spacing in between.
514 - 'equal': Each artist an equal fraction of the available space
515 and is left-aligned (or top-aligned) therein.
517 children : list of `.Artist`
518 The artists to pack.
520 Notes
521 -----
522 *pad* and *sep* need to given in points and will be scale with
523 the renderer dpi, while *width* and *height* need to be in
524 pixels.
525 """
526 super().__init__(pad, sep, width, height, align, mode, children)
528 def get_extent_offsets(self, renderer):
529 # docstring inherited
530 dpicor = renderer.points_to_pixels(1.)
531 pad = self.pad * dpicor
532 sep = self.sep * dpicor
534 whd_list = [c.get_extent(renderer)
535 for c in self.get_visible_children()]
537 if not whd_list:
538 return 2 * pad, 2 * pad, pad, pad, []
540 if self.height is None:
541 height_descent = max(h - yd for w, h, xd, yd in whd_list)
542 ydescent = max(yd for w, h, xd, yd in whd_list)
543 height = height_descent + ydescent
544 else:
545 height = self.height - 2 * pad # width w/o pad
547 hd_list = [(h, yd) for w, h, xd, yd in whd_list]
548 height, ydescent, yoffsets = _get_aligned_offsets(hd_list,
549 self.height,
550 self.align)
552 pack_list = [(w, xd) for w, h, xd, yd in whd_list]
554 width, xoffsets_ = _get_packed_offsets(pack_list, self.width,
555 sep, self.mode)
557 xoffsets = xoffsets_ + [xd for w, h, xd, yd in whd_list]
559 xdescent = whd_list[0][2]
560 xoffsets = xoffsets - xdescent
562 return (width + 2 * pad, height + 2 * pad,
563 xdescent + pad, ydescent + pad,
564 list(zip(xoffsets, yoffsets)))
567class PaddedBox(OffsetBox):
568 """
569 A container to add a padding around an `.Artist`.
571 The `.PaddedBox` contains a `.FancyBboxPatch` that is used to visualize
572 it when rendering.
573 """
574 def __init__(self, child, pad=None, draw_frame=False, patch_attrs=None):
575 """
576 Parameters
577 ----------
578 child : `~matplotlib.artist.Artist`
579 The contained `.Artist`.
580 pad : float
581 The padding in points. This will be scaled with the renderer dpi.
582 In contrast *width* and *hight* are in *pixel* and thus not scaled.
583 draw_frame : bool
584 Whether to draw the contained `.FancyBboxPatch`.
585 patch_attrs : dict or None
586 Additional parameters passed to the contained `.FancyBboxPatch`.
587 """
588 super().__init__()
590 self.pad = pad
591 self._children = [child]
593 self.patch = FancyBboxPatch(
594 xy=(0.0, 0.0), width=1., height=1.,
595 facecolor='w', edgecolor='k',
596 mutation_scale=1, # self.prop.get_size_in_points(),
597 snap=True
598 )
600 self.patch.set_boxstyle("square", pad=0)
602 if patch_attrs is not None:
603 self.patch.update(patch_attrs)
605 self._drawFrame = draw_frame
607 def get_extent_offsets(self, renderer):
608 # docstring inherited.
609 dpicor = renderer.points_to_pixels(1.)
610 pad = self.pad * dpicor
611 w, h, xd, yd = self._children[0].get_extent(renderer)
612 return (w + 2 * pad, h + 2 * pad, xd + pad, yd + pad,
613 [(0, 0)])
615 def draw(self, renderer):
616 """
617 Update the location of children if necessary and draw them
618 to the given *renderer*.
619 """
620 width, height, xdescent, ydescent, offsets = self.get_extent_offsets(
621 renderer)
623 px, py = self.get_offset(width, height, xdescent, ydescent, renderer)
625 for c, (ox, oy) in zip(self.get_visible_children(), offsets):
626 c.set_offset((px + ox, py + oy))
628 self.draw_frame(renderer)
630 for c in self.get_visible_children():
631 c.draw(renderer)
633 #bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
634 self.stale = False
636 def update_frame(self, bbox, fontsize=None):
637 self.patch.set_bounds(bbox.x0, bbox.y0,
638 bbox.width, bbox.height)
640 if fontsize:
641 self.patch.set_mutation_scale(fontsize)
642 self.stale = True
644 def draw_frame(self, renderer):
645 # update the location and size of the legend
646 bbox = self.get_window_extent(renderer)
647 self.update_frame(bbox)
649 if self._drawFrame:
650 self.patch.draw(renderer)
653class DrawingArea(OffsetBox):
654 """
655 The DrawingArea can contain any Artist as a child. The DrawingArea
656 has a fixed width and height. The position of children relative to
657 the parent is fixed. The children can be clipped at the
658 boundaries of the parent.
659 """
661 def __init__(self, width, height, xdescent=0.,
662 ydescent=0., clip=False):
663 """
664 *width*, *height* : width and height of the container box.
665 *xdescent*, *ydescent* : descent of the box in x- and y-direction.
666 *clip* : Whether to clip the children
667 """
668 super().__init__()
669 self.width = width
670 self.height = height
671 self.xdescent = xdescent
672 self.ydescent = ydescent
673 self._clip_children = clip
674 self.offset_transform = mtransforms.Affine2D()
675 self.dpi_transform = mtransforms.Affine2D()
677 @property
678 def clip_children(self):
679 """
680 If the children of this DrawingArea should be clipped
681 by DrawingArea bounding box.
682 """
683 return self._clip_children
685 @clip_children.setter
686 def clip_children(self, val):
687 self._clip_children = bool(val)
688 self.stale = True
690 def get_transform(self):
691 """
692 Return the `~matplotlib.transforms.Transform` applied to the children.
693 """
694 return self.dpi_transform + self.offset_transform
696 def set_transform(self, t):
697 """
698 set_transform is ignored.
699 """
701 def set_offset(self, xy):
702 """
703 Set the offset of the container.
705 Parameters
706 ----------
707 xy : (float, float)
708 The (x, y) coordinates of the offset in display units.
709 """
710 self._offset = xy
711 self.offset_transform.clear()
712 self.offset_transform.translate(xy[0], xy[1])
713 self.stale = True
715 def get_offset(self):
716 """
717 return offset of the container.
718 """
719 return self._offset
721 def get_window_extent(self, renderer):
722 '''
723 get the bounding box in display space.
724 '''
725 w, h, xd, yd = self.get_extent(renderer)
726 ox, oy = self.get_offset() # w, h, xd, yd)
728 return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h)
730 def get_extent(self, renderer):
731 """
732 Return with, height, xdescent, ydescent of box
733 """
735 dpi_cor = renderer.points_to_pixels(1.)
736 return (self.width * dpi_cor, self.height * dpi_cor,
737 self.xdescent * dpi_cor, self.ydescent * dpi_cor)
739 def add_artist(self, a):
740 'Add any :class:`~matplotlib.artist.Artist` to the container box'
741 self._children.append(a)
742 if not a.is_transform_set():
743 a.set_transform(self.get_transform())
744 if self.axes is not None:
745 a.axes = self.axes
746 fig = self.figure
747 if fig is not None:
748 a.set_figure(fig)
750 def draw(self, renderer):
751 """
752 Draw the children
753 """
755 dpi_cor = renderer.points_to_pixels(1.)
756 self.dpi_transform.clear()
757 self.dpi_transform.scale(dpi_cor)
759 # At this point the DrawingArea has a transform
760 # to the display space so the path created is
761 # good for clipping children
762 tpath = mtransforms.TransformedPath(
763 mpath.Path([[0, 0], [0, self.height],
764 [self.width, self.height],
765 [self.width, 0]]),
766 self.get_transform())
767 for c in self._children:
768 if self._clip_children and not (c.clipbox or c._clippath):
769 c.set_clip_path(tpath)
770 c.draw(renderer)
772 bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
773 self.stale = False
776class TextArea(OffsetBox):
777 """
778 The TextArea is contains a single Text instance. The text is
779 placed at (0, 0) with baseline+left alignment. The width and height
780 of the TextArea instance is the width and height of the its child
781 text.
782 """
783 def __init__(self, s,
784 textprops=None,
785 multilinebaseline=None,
786 minimumdescent=True,
787 ):
788 """
789 Parameters
790 ----------
791 s : str
792 a string to be displayed.
794 textprops : dictionary, optional, default: None
795 Dictionary of keyword parameters to be passed to the
796 `~matplotlib.text.Text` instance contained inside TextArea.
798 multilinebaseline : bool, optional
799 If `True`, baseline for multiline text is adjusted so that it is
800 (approximately) center-aligned with singleline text.
802 minimumdescent : bool, optional
803 If `True`, the box has a minimum descent of "p".
804 """
805 if textprops is None:
806 textprops = {}
807 textprops.setdefault("va", "baseline")
808 self._text = mtext.Text(0, 0, s, **textprops)
809 OffsetBox.__init__(self)
810 self._children = [self._text]
811 self.offset_transform = mtransforms.Affine2D()
812 self._baseline_transform = mtransforms.Affine2D()
813 self._text.set_transform(self.offset_transform +
814 self._baseline_transform)
815 self._multilinebaseline = multilinebaseline
816 self._minimumdescent = minimumdescent
818 def set_text(self, s):
819 "Set the text of this area as a string."
820 self._text.set_text(s)
821 self.stale = True
823 def get_text(self):
824 "Returns the string representation of this area's text"
825 return self._text.get_text()
827 def set_multilinebaseline(self, t):
828 """
829 Set multilinebaseline .
831 If True, baseline for multiline text is adjusted so that it is
832 (approximately) center-aligned with single-line text.
833 """
834 self._multilinebaseline = t
835 self.stale = True
837 def get_multilinebaseline(self):
838 """
839 get multilinebaseline .
840 """
841 return self._multilinebaseline
843 def set_minimumdescent(self, t):
844 """
845 Set minimumdescent .
847 If True, extent of the single line text is adjusted so that
848 it has minimum descent of "p"
849 """
850 self._minimumdescent = t
851 self.stale = True
853 def get_minimumdescent(self):
854 """
855 get minimumdescent.
856 """
857 return self._minimumdescent
859 def set_transform(self, t):
860 """
861 set_transform is ignored.
862 """
864 def set_offset(self, xy):
865 """
866 Set the offset of the container.
868 Parameters
869 ----------
870 xy : (float, float)
871 The (x, y) coordinates of the offset in display units.
872 """
873 self._offset = xy
874 self.offset_transform.clear()
875 self.offset_transform.translate(xy[0], xy[1])
876 self.stale = True
878 def get_offset(self):
879 """
880 return offset of the container.
881 """
882 return self._offset
884 def get_window_extent(self, renderer):
885 '''
886 get the bounding box in display space.
887 '''
888 w, h, xd, yd = self.get_extent(renderer)
889 ox, oy = self.get_offset() # w, h, xd, yd)
890 return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h)
892 def get_extent(self, renderer):
893 _, h_, d_ = renderer.get_text_width_height_descent(
894 "lp", self._text._fontproperties, ismath=False)
896 bbox, info, d = self._text._get_layout(renderer)
897 w, h = bbox.width, bbox.height
899 self._baseline_transform.clear()
901 if len(info) > 1 and self._multilinebaseline:
902 d_new = 0.5 * h - 0.5 * (h_ - d_)
903 self._baseline_transform.translate(0, d - d_new)
904 d = d_new
906 else: # single line
908 h_d = max(h_ - d_, h - d)
910 if self.get_minimumdescent():
911 ## to have a minimum descent, #i.e., "l" and "p" have same
912 ## descents.
913 d = max(d, d_)
914 #else:
915 # d = d
917 h = h_d + d
919 return w, h, 0., d
921 def draw(self, renderer):
922 """
923 Draw the children
924 """
926 self._text.draw(renderer)
928 bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
929 self.stale = False
932class AuxTransformBox(OffsetBox):
933 """
934 Offset Box with the aux_transform. Its children will be
935 transformed with the aux_transform first then will be
936 offseted. The absolute coordinate of the aux_transform is meaning
937 as it will be automatically adjust so that the left-lower corner
938 of the bounding box of children will be set to (0, 0) before the
939 offset transform.
941 It is similar to drawing area, except that the extent of the box
942 is not predetermined but calculated from the window extent of its
943 children. Furthermore, the extent of the children will be
944 calculated in the transformed coordinate.
945 """
946 def __init__(self, aux_transform):
947 self.aux_transform = aux_transform
948 OffsetBox.__init__(self)
949 self.offset_transform = mtransforms.Affine2D()
950 # ref_offset_transform makes offset_transform always relative to the
951 # lower-left corner of the bbox of its children.
952 self.ref_offset_transform = mtransforms.Affine2D()
954 def add_artist(self, a):
955 'Add any :class:`~matplotlib.artist.Artist` to the container box'
956 self._children.append(a)
957 a.set_transform(self.get_transform())
958 self.stale = True
960 def get_transform(self):
961 """
962 Return the :class:`~matplotlib.transforms.Transform` applied
963 to the children
964 """
965 return (self.aux_transform
966 + self.ref_offset_transform
967 + self.offset_transform)
969 def set_transform(self, t):
970 """
971 set_transform is ignored.
972 """
974 def set_offset(self, xy):
975 """
976 Set the offset of the container.
978 Parameters
979 ----------
980 xy : (float, float)
981 The (x, y) coordinates of the offset in display units.
982 """
983 self._offset = xy
984 self.offset_transform.clear()
985 self.offset_transform.translate(xy[0], xy[1])
986 self.stale = True
988 def get_offset(self):
989 """
990 return offset of the container.
991 """
992 return self._offset
994 def get_window_extent(self, renderer):
995 '''
996 get the bounding box in display space.
997 '''
998 w, h, xd, yd = self.get_extent(renderer)
999 ox, oy = self.get_offset() # w, h, xd, yd)
1000 return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h)
1002 def get_extent(self, renderer):
1003 # clear the offset transforms
1004 _off = self.offset_transform.get_matrix() # to be restored later
1005 self.ref_offset_transform.clear()
1006 self.offset_transform.clear()
1007 # calculate the extent
1008 bboxes = [c.get_window_extent(renderer) for c in self._children]
1009 ub = mtransforms.Bbox.union(bboxes)
1010 # adjust ref_offset_transform
1011 self.ref_offset_transform.translate(-ub.x0, -ub.y0)
1012 # restor offset transform
1013 self.offset_transform.set_matrix(_off)
1015 return ub.width, ub.height, 0., 0.
1017 def draw(self, renderer):
1018 """
1019 Draw the children
1020 """
1022 for c in self._children:
1023 c.draw(renderer)
1025 bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
1026 self.stale = False
1029class AnchoredOffsetbox(OffsetBox):
1030 """
1031 An offset box placed according to the legend location
1032 loc. AnchoredOffsetbox has a single child. When multiple children
1033 is needed, use other OffsetBox class to enclose them. By default,
1034 the offset box is anchored against its parent axes. You may
1035 explicitly specify the bbox_to_anchor.
1036 """
1037 zorder = 5 # zorder of the legend
1039 # Location codes
1040 codes = {'upper right': 1,
1041 'upper left': 2,
1042 'lower left': 3,
1043 'lower right': 4,
1044 'right': 5,
1045 'center left': 6,
1046 'center right': 7,
1047 'lower center': 8,
1048 'upper center': 9,
1049 'center': 10,
1050 }
1052 def __init__(self, loc,
1053 pad=0.4, borderpad=0.5,
1054 child=None, prop=None, frameon=True,
1055 bbox_to_anchor=None,
1056 bbox_transform=None,
1057 **kwargs):
1058 """
1059 loc is a string or an integer specifying the legend location.
1060 The valid location codes are::
1062 'upper right' : 1,
1063 'upper left' : 2,
1064 'lower left' : 3,
1065 'lower right' : 4,
1066 'right' : 5, (same as 'center right', for back-compatibility)
1067 'center left' : 6,
1068 'center right' : 7,
1069 'lower center' : 8,
1070 'upper center' : 9,
1071 'center' : 10,
1073 pad : pad around the child for drawing a frame. given in
1074 fraction of fontsize.
1076 borderpad : pad between offsetbox frame and the bbox_to_anchor,
1078 child : OffsetBox instance that will be anchored.
1080 prop : font property. This is only used as a reference for paddings.
1082 frameon : draw a frame box if True.
1084 bbox_to_anchor : bbox to anchor. Use self.axes.bbox if None.
1086 bbox_transform : with which the bbox_to_anchor will be transformed.
1088 """
1089 super().__init__(**kwargs)
1091 self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform)
1092 self.set_child(child)
1094 if isinstance(loc, str):
1095 loc = cbook._check_getitem(self.codes, loc=loc)
1097 self.loc = loc
1098 self.borderpad = borderpad
1099 self.pad = pad
1101 if prop is None:
1102 self.prop = FontProperties(size=rcParams["legend.fontsize"])
1103 elif isinstance(prop, dict):
1104 self.prop = FontProperties(**prop)
1105 if "size" not in prop:
1106 self.prop.set_size(rcParams["legend.fontsize"])
1107 else:
1108 self.prop = prop
1110 self.patch = FancyBboxPatch(
1111 xy=(0.0, 0.0), width=1., height=1.,
1112 facecolor='w', edgecolor='k',
1113 mutation_scale=self.prop.get_size_in_points(),
1114 snap=True
1115 )
1116 self.patch.set_boxstyle("square", pad=0)
1117 self._drawFrame = frameon
1119 def set_child(self, child):
1120 "set the child to be anchored"
1121 self._child = child
1122 if child is not None:
1123 child.axes = self.axes
1124 self.stale = True
1126 def get_child(self):
1127 "return the child"
1128 return self._child
1130 def get_children(self):
1131 "return the list of children"
1132 return [self._child]
1134 def get_extent(self, renderer):
1135 """
1136 return the extent of the artist. The extent of the child
1137 added with the pad is returned
1138 """
1139 w, h, xd, yd = self.get_child().get_extent(renderer)
1140 fontsize = renderer.points_to_pixels(self.prop.get_size_in_points())
1141 pad = self.pad * fontsize
1143 return w + 2 * pad, h + 2 * pad, xd + pad, yd + pad
1145 def get_bbox_to_anchor(self):
1146 """
1147 return the bbox that the legend will be anchored
1148 """
1149 if self._bbox_to_anchor is None:
1150 return self.axes.bbox
1151 else:
1152 transform = self._bbox_to_anchor_transform
1153 if transform is None:
1154 return self._bbox_to_anchor
1155 else:
1156 return TransformedBbox(self._bbox_to_anchor,
1157 transform)
1159 def set_bbox_to_anchor(self, bbox, transform=None):
1160 """
1161 set the bbox that the child will be anchored.
1163 *bbox* can be a Bbox instance, a list of [left, bottom, width,
1164 height], or a list of [left, bottom] where the width and
1165 height will be assumed to be zero. The bbox will be
1166 transformed to display coordinate by the given transform.
1167 """
1168 if bbox is None or isinstance(bbox, BboxBase):
1169 self._bbox_to_anchor = bbox
1170 else:
1171 try:
1172 l = len(bbox)
1173 except TypeError:
1174 raise ValueError("Invalid argument for bbox : %s" % str(bbox))
1176 if l == 2:
1177 bbox = [bbox[0], bbox[1], 0, 0]
1179 self._bbox_to_anchor = Bbox.from_bounds(*bbox)
1181 self._bbox_to_anchor_transform = transform
1182 self.stale = True
1184 def get_window_extent(self, renderer):
1185 '''
1186 get the bounding box in display space.
1187 '''
1188 self._update_offset_func(renderer)
1189 w, h, xd, yd = self.get_extent(renderer)
1190 ox, oy = self.get_offset(w, h, xd, yd, renderer)
1191 return Bbox.from_bounds(ox - xd, oy - yd, w, h)
1193 def _update_offset_func(self, renderer, fontsize=None):
1194 """
1195 Update the offset func which depends on the dpi of the
1196 renderer (because of the padding).
1197 """
1198 if fontsize is None:
1199 fontsize = renderer.points_to_pixels(
1200 self.prop.get_size_in_points())
1202 def _offset(w, h, xd, yd, renderer, fontsize=fontsize, self=self):
1203 bbox = Bbox.from_bounds(0, 0, w, h)
1204 borderpad = self.borderpad * fontsize
1205 bbox_to_anchor = self.get_bbox_to_anchor()
1207 x0, y0 = self._get_anchored_bbox(self.loc,
1208 bbox,
1209 bbox_to_anchor,
1210 borderpad)
1211 return x0 + xd, y0 + yd
1213 self.set_offset(_offset)
1215 def update_frame(self, bbox, fontsize=None):
1216 self.patch.set_bounds(bbox.x0, bbox.y0,
1217 bbox.width, bbox.height)
1219 if fontsize:
1220 self.patch.set_mutation_scale(fontsize)
1222 def draw(self, renderer):
1223 "draw the artist"
1225 if not self.get_visible():
1226 return
1228 fontsize = renderer.points_to_pixels(self.prop.get_size_in_points())
1229 self._update_offset_func(renderer, fontsize)
1231 if self._drawFrame:
1232 # update the location and size of the legend
1233 bbox = self.get_window_extent(renderer)
1234 self.update_frame(bbox, fontsize)
1235 self.patch.draw(renderer)
1237 width, height, xdescent, ydescent = self.get_extent(renderer)
1239 px, py = self.get_offset(width, height, xdescent, ydescent, renderer)
1241 self.get_child().set_offset((px, py))
1242 self.get_child().draw(renderer)
1243 self.stale = False
1245 def _get_anchored_bbox(self, loc, bbox, parentbbox, borderpad):
1246 """
1247 return the position of the bbox anchored at the parentbbox
1248 with the loc code, with the borderpad.
1249 """
1250 assert loc in range(1, 11) # called only internally
1252 BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11)
1254 anchor_coefs = {UR: "NE",
1255 UL: "NW",
1256 LL: "SW",
1257 LR: "SE",
1258 R: "E",
1259 CL: "W",
1260 CR: "E",
1261 LC: "S",
1262 UC: "N",
1263 C: "C"}
1265 c = anchor_coefs[loc]
1267 container = parentbbox.padded(-borderpad)
1268 anchored_box = bbox.anchored(c, container=container)
1269 return anchored_box.x0, anchored_box.y0
1272class AnchoredText(AnchoredOffsetbox):
1273 """
1274 AnchoredOffsetbox with Text.
1275 """
1277 def __init__(self, s, loc, pad=0.4, borderpad=0.5, prop=None, **kwargs):
1278 """
1279 Parameters
1280 ----------
1281 s : str
1282 Text.
1284 loc : str
1285 Location code.
1287 pad : float, optional
1288 Pad between the text and the frame as fraction of the font
1289 size.
1291 borderpad : float, optional
1292 Pad between the frame and the axes (or *bbox_to_anchor*).
1294 prop : dictionary, optional, default: None
1295 Dictionary of keyword parameters to be passed to the
1296 `~matplotlib.text.Text` instance contained inside AnchoredText.
1298 Notes
1299 -----
1300 Other keyword parameters of `AnchoredOffsetbox` are also
1301 allowed.
1302 """
1304 if prop is None:
1305 prop = {}
1306 badkwargs = {'ha', 'horizontalalignment', 'va', 'verticalalignment'}
1307 if badkwargs & set(prop):
1308 cbook.warn_deprecated(
1309 "3.1", message="Mixing horizontalalignment or "
1310 "verticalalignment with AnchoredText is not supported, "
1311 "deprecated since %(since)s, and will raise an exception "
1312 "%(removal)s.")
1314 self.txt = TextArea(s, textprops=prop, minimumdescent=False)
1315 fp = self.txt._text.get_fontproperties()
1316 super().__init__(
1317 loc, pad=pad, borderpad=borderpad, child=self.txt, prop=fp,
1318 **kwargs)
1321class OffsetImage(OffsetBox):
1322 def __init__(self, arr,
1323 zoom=1,
1324 cmap=None,
1325 norm=None,
1326 interpolation=None,
1327 origin=None,
1328 filternorm=1,
1329 filterrad=4.0,
1330 resample=False,
1331 dpi_cor=True,
1332 **kwargs
1333 ):
1335 OffsetBox.__init__(self)
1336 self._dpi_cor = dpi_cor
1338 self.image = BboxImage(bbox=self.get_window_extent,
1339 cmap=cmap,
1340 norm=norm,
1341 interpolation=interpolation,
1342 origin=origin,
1343 filternorm=filternorm,
1344 filterrad=filterrad,
1345 resample=resample,
1346 **kwargs
1347 )
1349 self._children = [self.image]
1351 self.set_zoom(zoom)
1352 self.set_data(arr)
1354 def set_data(self, arr):
1355 self._data = np.asarray(arr)
1356 self.image.set_data(self._data)
1357 self.stale = True
1359 def get_data(self):
1360 return self._data
1362 def set_zoom(self, zoom):
1363 self._zoom = zoom
1364 self.stale = True
1366 def get_zoom(self):
1367 return self._zoom
1369# def set_axes(self, axes):
1370# self.image.set_axes(axes)
1371# martist.Artist.set_axes(self, axes)
1373# def set_offset(self, xy):
1374# """
1375# Set the offset of the container.
1376#
1377# Parameters
1378# ----------
1379# xy : (float, float)
1380# The (x, y) coordinates of the offset in display units.
1381# """
1382# self._offset = xy
1384# self.offset_transform.clear()
1385# self.offset_transform.translate(xy[0], xy[1])
1387 def get_offset(self):
1388 """
1389 return offset of the container.
1390 """
1391 return self._offset
1393 def get_children(self):
1394 return [self.image]
1396 def get_window_extent(self, renderer):
1397 '''
1398 get the bounding box in display space.
1399 '''
1400 w, h, xd, yd = self.get_extent(renderer)
1401 ox, oy = self.get_offset()
1402 return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h)
1404 def get_extent(self, renderer):
1405 if self._dpi_cor: # True, do correction
1406 dpi_cor = renderer.points_to_pixels(1.)
1407 else:
1408 dpi_cor = 1.
1410 zoom = self.get_zoom()
1411 data = self.get_data()
1412 ny, nx = data.shape[:2]
1413 w, h = dpi_cor * nx * zoom, dpi_cor * ny * zoom
1415 return w, h, 0, 0
1417 def draw(self, renderer):
1418 """
1419 Draw the children
1420 """
1421 self.image.draw(renderer)
1422 # bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
1423 self.stale = False
1426class AnnotationBbox(martist.Artist, _AnnotationBase):
1427 """
1428 Annotation-like class, but with offsetbox instead of Text.
1429 """
1430 zorder = 3
1432 def __str__(self):
1433 return "AnnotationBbox(%g,%g)" % (self.xy[0], self.xy[1])
1435 @docstring.dedent_interpd
1436 def __init__(self, offsetbox, xy,
1437 xybox=None,
1438 xycoords='data',
1439 boxcoords=None,
1440 frameon=True, pad=0.4, # BboxPatch
1441 annotation_clip=None,
1442 box_alignment=(0.5, 0.5),
1443 bboxprops=None,
1444 arrowprops=None,
1445 fontsize=None,
1446 **kwargs):
1447 """
1448 *offsetbox* : OffsetBox instance
1450 *xycoords* : same as Annotation but can be a tuple of two
1451 strings which are interpreted as x and y coordinates.
1453 *boxcoords* : similar to textcoords as Annotation but can be a
1454 tuple of two strings which are interpreted as x and y
1455 coordinates.
1457 *box_alignment* : a tuple of two floats for a vertical and
1458 horizontal alignment of the offset box w.r.t. the *boxcoords*.
1459 The lower-left corner is (0.0) and upper-right corner is (1.1).
1461 other parameters are identical to that of Annotation.
1462 """
1464 martist.Artist.__init__(self, **kwargs)
1465 _AnnotationBase.__init__(self,
1466 xy,
1467 xycoords=xycoords,
1468 annotation_clip=annotation_clip)
1470 self.offsetbox = offsetbox
1472 self.arrowprops = arrowprops
1474 self.set_fontsize(fontsize)
1476 if xybox is None:
1477 self.xybox = xy
1478 else:
1479 self.xybox = xybox
1481 if boxcoords is None:
1482 self.boxcoords = xycoords
1483 else:
1484 self.boxcoords = boxcoords
1486 if arrowprops is not None:
1487 self._arrow_relpos = self.arrowprops.pop("relpos", (0.5, 0.5))
1488 self.arrow_patch = FancyArrowPatch((0, 0), (1, 1),
1489 **self.arrowprops)
1490 else:
1491 self._arrow_relpos = None
1492 self.arrow_patch = None
1494 self._box_alignment = box_alignment
1496 # frame
1497 self.patch = FancyBboxPatch(
1498 xy=(0.0, 0.0), width=1., height=1.,
1499 facecolor='w', edgecolor='k',
1500 mutation_scale=self.prop.get_size_in_points(),
1501 snap=True
1502 )
1503 self.patch.set_boxstyle("square", pad=pad)
1504 if bboxprops:
1505 self.patch.set(**bboxprops)
1506 self._drawFrame = frameon
1508 @property
1509 def xyann(self):
1510 return self.xybox
1512 @xyann.setter
1513 def xyann(self, xyann):
1514 self.xybox = xyann
1515 self.stale = True
1517 @property
1518 def anncoords(self):
1519 return self.boxcoords
1521 @anncoords.setter
1522 def anncoords(self, coords):
1523 self.boxcoords = coords
1524 self.stale = True
1526 def contains(self, mouseevent):
1527 inside, info = self._default_contains(mouseevent)
1528 if inside is not None:
1529 return inside, info
1530 t, tinfo = self.offsetbox.contains(mouseevent)
1531 #if self.arrow_patch is not None:
1532 # a, ainfo=self.arrow_patch.contains(event)
1533 # t = t or a
1535 # self.arrow_patch is currently not checked as this can be a line - JJ
1537 return t, tinfo
1539 def get_children(self):
1540 children = [self.offsetbox, self.patch]
1541 if self.arrow_patch:
1542 children.append(self.arrow_patch)
1543 return children
1545 def set_figure(self, fig):
1547 if self.arrow_patch is not None:
1548 self.arrow_patch.set_figure(fig)
1549 self.offsetbox.set_figure(fig)
1550 martist.Artist.set_figure(self, fig)
1552 def set_fontsize(self, s=None):
1553 """
1554 set fontsize in points
1555 """
1556 if s is None:
1557 s = rcParams["legend.fontsize"]
1559 self.prop = FontProperties(size=s)
1560 self.stale = True
1562 def get_fontsize(self, s=None):
1563 """
1564 return fontsize in points
1565 """
1566 return self.prop.get_size_in_points()
1568 def update_positions(self, renderer):
1569 """
1570 Update the pixel positions of the annotated point and the text.
1571 """
1572 xy_pixel = self._get_position_xy(renderer)
1573 self._update_position_xybox(renderer, xy_pixel)
1575 mutation_scale = renderer.points_to_pixels(self.get_fontsize())
1576 self.patch.set_mutation_scale(mutation_scale)
1578 if self.arrow_patch:
1579 self.arrow_patch.set_mutation_scale(mutation_scale)
1581 def _update_position_xybox(self, renderer, xy_pixel):
1582 """
1583 Update the pixel positions of the annotation text and the arrow
1584 patch.
1585 """
1587 x, y = self.xybox
1588 if isinstance(self.boxcoords, tuple):
1589 xcoord, ycoord = self.boxcoords
1590 x1, y1 = self._get_xy(renderer, x, y, xcoord)
1591 x2, y2 = self._get_xy(renderer, x, y, ycoord)
1592 ox0, oy0 = x1, y2
1593 else:
1594 ox0, oy0 = self._get_xy(renderer, x, y, self.boxcoords)
1596 w, h, xd, yd = self.offsetbox.get_extent(renderer)
1598 _fw, _fh = self._box_alignment
1599 self.offsetbox.set_offset((ox0 - _fw * w + xd, oy0 - _fh * h + yd))
1601 # update patch position
1602 bbox = self.offsetbox.get_window_extent(renderer)
1603 #self.offsetbox.set_offset((ox0-_fw*w, oy0-_fh*h))
1604 self.patch.set_bounds(bbox.x0, bbox.y0,
1605 bbox.width, bbox.height)
1607 x, y = xy_pixel
1609 ox1, oy1 = x, y
1611 if self.arrowprops:
1612 d = self.arrowprops.copy()
1614 # Use FancyArrowPatch if self.arrowprops has "arrowstyle" key.
1616 # adjust the starting point of the arrow relative to
1617 # the textbox.
1618 # TODO : Rotation needs to be accounted.
1619 relpos = self._arrow_relpos
1621 ox0 = bbox.x0 + bbox.width * relpos[0]
1622 oy0 = bbox.y0 + bbox.height * relpos[1]
1624 # The arrow will be drawn from (ox0, oy0) to (ox1,
1625 # oy1). It will be first clipped by patchA and patchB.
1626 # Then it will be shrunk by shrinkA and shrinkB
1627 # (in points). If patch A is not set, self.bbox_patch
1628 # is used.
1630 self.arrow_patch.set_positions((ox0, oy0), (ox1, oy1))
1631 fs = self.prop.get_size_in_points()
1632 mutation_scale = d.pop("mutation_scale", fs)
1633 mutation_scale = renderer.points_to_pixels(mutation_scale)
1634 self.arrow_patch.set_mutation_scale(mutation_scale)
1636 patchA = d.pop("patchA", self.patch)
1637 self.arrow_patch.set_patchA(patchA)
1639 def draw(self, renderer):
1640 """
1641 Draw the :class:`Annotation` object to the given *renderer*.
1642 """
1644 if renderer is not None:
1645 self._renderer = renderer
1646 if not self.get_visible():
1647 return
1649 xy_pixel = self._get_position_xy(renderer)
1651 if not self._check_xy(renderer, xy_pixel):
1652 return
1654 self.update_positions(renderer)
1656 if self.arrow_patch is not None:
1657 if self.arrow_patch.figure is None and self.figure is not None:
1658 self.arrow_patch.figure = self.figure
1659 self.arrow_patch.draw(renderer)
1661 if self._drawFrame:
1662 self.patch.draw(renderer)
1664 self.offsetbox.draw(renderer)
1665 self.stale = False
1668class DraggableBase:
1669 """
1670 Helper base class for a draggable artist (legend, offsetbox).
1672 Derived classes must override the following methods::
1674 def save_offset(self):
1675 '''
1676 Called when the object is picked for dragging; should save the
1677 reference position of the artist.
1678 '''
1680 def update_offset(self, dx, dy):
1681 '''
1682 Called during the dragging; (*dx*, *dy*) is the pixel offset from
1683 the point where the mouse drag started.
1684 '''
1686 Optionally, you may override the following methods::
1688 def artist_picker(self, artist, evt):
1689 '''The picker method that will be used.'''
1690 return self.ref_artist.contains(evt)
1692 def finalize_offset(self):
1693 '''Called when the mouse is released.'''
1695 In the current implementation of `DraggableLegend` and
1696 `DraggableAnnotation`, `update_offset` places the artists in display
1697 coordinates, and `finalize_offset` recalculates their position in axes
1698 coordinate and set a relevant attribute.
1699 """
1701 def __init__(self, ref_artist, use_blit=False):
1702 self.ref_artist = ref_artist
1703 self.got_artist = False
1705 self.canvas = self.ref_artist.figure.canvas
1706 self._use_blit = use_blit and self.canvas.supports_blit
1708 c2 = self.canvas.mpl_connect('pick_event', self.on_pick)
1709 c3 = self.canvas.mpl_connect('button_release_event', self.on_release)
1711 ref_artist.set_picker(self.artist_picker)
1712 self.cids = [c2, c3]
1714 def on_motion(self, evt):
1715 if self._check_still_parented() and self.got_artist:
1716 dx = evt.x - self.mouse_x
1717 dy = evt.y - self.mouse_y
1718 self.update_offset(dx, dy)
1719 self.canvas.draw()
1721 def on_motion_blit(self, evt):
1722 if self._check_still_parented() and self.got_artist:
1723 dx = evt.x - self.mouse_x
1724 dy = evt.y - self.mouse_y
1725 self.update_offset(dx, dy)
1726 self.canvas.restore_region(self.background)
1727 self.ref_artist.draw(self.ref_artist.figure._cachedRenderer)
1728 self.canvas.blit()
1730 def on_pick(self, evt):
1731 if self._check_still_parented() and evt.artist == self.ref_artist:
1733 self.mouse_x = evt.mouseevent.x
1734 self.mouse_y = evt.mouseevent.y
1735 self.got_artist = True
1737 if self._use_blit:
1738 self.ref_artist.set_animated(True)
1739 self.canvas.draw()
1740 self.background = self.canvas.copy_from_bbox(
1741 self.ref_artist.figure.bbox)
1742 self.ref_artist.draw(self.ref_artist.figure._cachedRenderer)
1743 self.canvas.blit()
1744 self._c1 = self.canvas.mpl_connect('motion_notify_event',
1745 self.on_motion_blit)
1746 else:
1747 self._c1 = self.canvas.mpl_connect('motion_notify_event',
1748 self.on_motion)
1749 self.save_offset()
1751 def on_release(self, event):
1752 if self._check_still_parented() and self.got_artist:
1753 self.finalize_offset()
1754 self.got_artist = False
1755 self.canvas.mpl_disconnect(self._c1)
1757 if self._use_blit:
1758 self.ref_artist.set_animated(False)
1760 def _check_still_parented(self):
1761 if self.ref_artist.figure is None:
1762 self.disconnect()
1763 return False
1764 else:
1765 return True
1767 def disconnect(self):
1768 """Disconnect the callbacks."""
1769 for cid in self.cids:
1770 self.canvas.mpl_disconnect(cid)
1771 try:
1772 c1 = self._c1
1773 except AttributeError:
1774 pass
1775 else:
1776 self.canvas.mpl_disconnect(c1)
1778 def artist_picker(self, artist, evt):
1779 return self.ref_artist.contains(evt)
1781 def save_offset(self):
1782 pass
1784 def update_offset(self, dx, dy):
1785 pass
1787 def finalize_offset(self):
1788 pass
1791class DraggableOffsetBox(DraggableBase):
1792 def __init__(self, ref_artist, offsetbox, use_blit=False):
1793 DraggableBase.__init__(self, ref_artist, use_blit=use_blit)
1794 self.offsetbox = offsetbox
1796 def save_offset(self):
1797 offsetbox = self.offsetbox
1798 renderer = offsetbox.figure._cachedRenderer
1799 w, h, xd, yd = offsetbox.get_extent(renderer)
1800 offset = offsetbox.get_offset(w, h, xd, yd, renderer)
1801 self.offsetbox_x, self.offsetbox_y = offset
1802 self.offsetbox.set_offset(offset)
1804 def update_offset(self, dx, dy):
1805 loc_in_canvas = self.offsetbox_x + dx, self.offsetbox_y + dy
1806 self.offsetbox.set_offset(loc_in_canvas)
1808 def get_loc_in_canvas(self):
1809 offsetbox = self.offsetbox
1810 renderer = offsetbox.figure._cachedRenderer
1811 w, h, xd, yd = offsetbox.get_extent(renderer)
1812 ox, oy = offsetbox._offset
1813 loc_in_canvas = (ox - xd, oy - yd)
1814 return loc_in_canvas
1817class DraggableAnnotation(DraggableBase):
1818 def __init__(self, annotation, use_blit=False):
1819 DraggableBase.__init__(self, annotation, use_blit=use_blit)
1820 self.annotation = annotation
1822 def save_offset(self):
1823 ann = self.annotation
1824 self.ox, self.oy = ann.get_transform().transform(ann.xyann)
1826 def update_offset(self, dx, dy):
1827 ann = self.annotation
1828 ann.xyann = ann.get_transform().inverted().transform(
1829 (self.ox + dx, self.oy + dy))