| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613 |
- r"""
- Container classes for `.Artist`\s.
- `OffsetBox`
- The base of all container artists defined in this module.
- `AnchoredOffsetbox`, `AnchoredText`
- Anchor and align an arbitrary `.Artist` or a text relative to the parent
- axes or a specific anchor point.
- `DrawingArea`
- A container with fixed width and height. Children have a fixed position
- inside the container and may be clipped.
- `HPacker`, `VPacker`
- Containers for layouting their children vertically or horizontally.
- `PaddedBox`
- A container to add a padding around an `.Artist`.
- `TextArea`
- Contains a single `.Text` instance.
- """
- import functools
- import numpy as np
- import matplotlib as mpl
- from matplotlib import _api, _docstring
- import matplotlib.artist as martist
- import matplotlib.path as mpath
- import matplotlib.text as mtext
- import matplotlib.transforms as mtransforms
- from matplotlib.font_manager import FontProperties
- from matplotlib.image import BboxImage
- from matplotlib.patches import (
- FancyBboxPatch, FancyArrowPatch, bbox_artist as mbbox_artist)
- from matplotlib.transforms import Bbox, BboxBase, TransformedBbox
- DEBUG = False
- def _compat_get_offset(meth):
- """
- Decorator for the get_offset method of OffsetBox and subclasses, that
- allows supporting both the new signature (self, bbox, renderer) and the old
- signature (self, width, height, xdescent, ydescent, renderer).
- """
- sigs = [lambda self, width, height, xdescent, ydescent, renderer: locals(),
- lambda self, bbox, renderer: locals()]
- @functools.wraps(meth)
- def get_offset(self, *args, **kwargs):
- params = _api.select_matching_signature(sigs, self, *args, **kwargs)
- bbox = (params["bbox"] if "bbox" in params else
- Bbox.from_bounds(-params["xdescent"], -params["ydescent"],
- params["width"], params["height"]))
- return meth(params["self"], bbox, params["renderer"])
- return get_offset
- # for debugging use
- def _bbox_artist(*args, **kwargs):
- if DEBUG:
- mbbox_artist(*args, **kwargs)
- def _get_packed_offsets(widths, total, sep, mode="fixed"):
- r"""
- Pack boxes specified by their *widths*.
- For simplicity of the description, the terminology used here assumes a
- horizontal layout, but the function works equally for a vertical layout.
- There are three packing *mode*\s:
- - 'fixed': The elements are packed tight to the left with a spacing of
- *sep* in between. If *total* is *None* the returned total will be the
- right edge of the last box. A non-*None* total will be passed unchecked
- to the output. In particular this means that right edge of the last
- box may be further to the right than the returned total.
- - 'expand': Distribute the boxes with equal spacing so that the left edge
- of the first box is at 0, and the right edge of the last box is at
- *total*. The parameter *sep* is ignored in this mode. A total of *None*
- is accepted and considered equal to 1. The total is returned unchanged
- (except for the conversion *None* to 1). If the total is smaller than
- the sum of the widths, the laid out boxes will overlap.
- - 'equal': If *total* is given, the total space is divided in N equal
- ranges and each box is left-aligned within its subspace.
- Otherwise (*total* is *None*), *sep* must be provided and each box is
- left-aligned in its subspace of width ``(max(widths) + sep)``. The
- total width is then calculated to be ``N * (max(widths) + sep)``.
- Parameters
- ----------
- widths : list of float
- Widths of boxes to be packed.
- total : float or None
- Intended total length. *None* if not used.
- sep : float or None
- Spacing between boxes.
- mode : {'fixed', 'expand', 'equal'}
- The packing mode.
- Returns
- -------
- total : float
- The total width needed to accommodate the laid out boxes.
- offsets : array of float
- The left offsets of the boxes.
- """
- _api.check_in_list(["fixed", "expand", "equal"], mode=mode)
- if mode == "fixed":
- offsets_ = np.cumsum([0] + [w + sep for w in widths])
- offsets = offsets_[:-1]
- if total is None:
- total = offsets_[-1] - sep
- return total, offsets
- elif mode == "expand":
- # This is a bit of a hack to avoid a TypeError when *total*
- # is None and used in conjugation with tight layout.
- if total is None:
- total = 1
- if len(widths) > 1:
- sep = (total - sum(widths)) / (len(widths) - 1)
- else:
- sep = 0
- offsets_ = np.cumsum([0] + [w + sep for w in widths])
- offsets = offsets_[:-1]
- return total, offsets
- elif mode == "equal":
- maxh = max(widths)
- if total is None:
- if sep is None:
- raise ValueError("total and sep cannot both be None when "
- "using layout mode 'equal'")
- total = (maxh + sep) * len(widths)
- else:
- sep = total / len(widths) - maxh
- offsets = (maxh + sep) * np.arange(len(widths))
- return total, offsets
- def _get_aligned_offsets(yspans, height, align="baseline"):
- """
- Align boxes each specified by their ``(y0, y1)`` spans.
- For simplicity of the description, the terminology used here assumes a
- horizontal layout (i.e., vertical alignment), but the function works
- equally for a vertical layout.
- Parameters
- ----------
- yspans
- List of (y0, y1) spans of boxes to be aligned.
- height : float or None
- Intended total height. If None, the maximum of the heights
- (``y1 - y0``) in *yspans* is used.
- align : {'baseline', 'left', 'top', 'right', 'bottom', 'center'}
- The alignment anchor of the boxes.
- Returns
- -------
- (y0, y1)
- y range spanned by the packing. If a *height* was originally passed
- in, then for all alignments other than "baseline", a span of ``(0,
- height)`` is used without checking that it is actually large enough).
- descent
- The descent of the packing.
- offsets
- The bottom offsets of the boxes.
- """
- _api.check_in_list(
- ["baseline", "left", "top", "right", "bottom", "center"], align=align)
- if height is None:
- height = max(y1 - y0 for y0, y1 in yspans)
- if align == "baseline":
- yspan = (min(y0 for y0, y1 in yspans), max(y1 for y0, y1 in yspans))
- offsets = [0] * len(yspans)
- elif align in ["left", "bottom"]:
- yspan = (0, height)
- offsets = [-y0 for y0, y1 in yspans]
- elif align in ["right", "top"]:
- yspan = (0, height)
- offsets = [height - y1 for y0, y1 in yspans]
- elif align == "center":
- yspan = (0, height)
- offsets = [(height - (y1 - y0)) * .5 - y0 for y0, y1 in yspans]
- return yspan, offsets
- class OffsetBox(martist.Artist):
- """
- A simple container artist.
- The child artists are meant to be drawn at a relative position to its
- parent.
- Being an artist itself, all parameters are passed on to `.Artist`.
- """
- def __init__(self, *args, **kwargs):
- super().__init__(*args)
- self._internal_update(kwargs)
- # Clipping has not been implemented in the OffsetBox family, so
- # disable the clip flag for consistency. It can always be turned back
- # on to zero effect.
- self.set_clip_on(False)
- self._children = []
- self._offset = (0, 0)
- def set_figure(self, fig):
- """
- Set the `.Figure` for the `.OffsetBox` and all its children.
- Parameters
- ----------
- fig : `~matplotlib.figure.Figure`
- """
- super().set_figure(fig)
- for c in self.get_children():
- c.set_figure(fig)
- @martist.Artist.axes.setter
- def axes(self, ax):
- # TODO deal with this better
- martist.Artist.axes.fset(self, ax)
- for c in self.get_children():
- if c is not None:
- c.axes = ax
- def contains(self, mouseevent):
- """
- Delegate the mouse event contains-check to the children.
- As a container, the `.OffsetBox` does not respond itself to
- mouseevents.
- Parameters
- ----------
- mouseevent : `~matplotlib.backend_bases.MouseEvent`
- Returns
- -------
- contains : bool
- Whether any values are within the radius.
- details : dict
- An artist-specific dictionary of details of the event context,
- such as which points are contained in the pick radius. See the
- individual Artist subclasses for details.
- See Also
- --------
- .Artist.contains
- """
- if self._different_canvas(mouseevent):
- return False, {}
- for c in self.get_children():
- a, b = c.contains(mouseevent)
- if a:
- return a, b
- return False, {}
- def set_offset(self, xy):
- """
- Set the offset.
- Parameters
- ----------
- xy : (float, float) or callable
- The (x, y) coordinates of the offset in display units. These can
- either be given explicitly as a tuple (x, y), or by providing a
- function that converts the extent into the offset. This function
- must have the signature::
- def offset(width, height, xdescent, ydescent, renderer) \
- -> (float, float)
- """
- self._offset = xy
- self.stale = True
- @_compat_get_offset
- def get_offset(self, bbox, renderer):
- """
- Return the offset as a tuple (x, y).
- The extent parameters have to be provided to handle the case where the
- offset is dynamically determined by a callable (see
- `~.OffsetBox.set_offset`).
- Parameters
- ----------
- bbox : `.Bbox`
- renderer : `.RendererBase` subclass
- """
- return (
- self._offset(bbox.width, bbox.height, -bbox.x0, -bbox.y0, renderer)
- if callable(self._offset)
- else self._offset)
- def set_width(self, width):
- """
- Set the width of the box.
- Parameters
- ----------
- width : float
- """
- self.width = width
- self.stale = True
- def set_height(self, height):
- """
- Set the height of the box.
- Parameters
- ----------
- height : float
- """
- self.height = height
- self.stale = True
- def get_visible_children(self):
- r"""Return a list of the visible child `.Artist`\s."""
- return [c for c in self._children if c.get_visible()]
- def get_children(self):
- r"""Return a list of the child `.Artist`\s."""
- return self._children
- def _get_bbox_and_child_offsets(self, renderer):
- """
- Return the bbox of the offsetbox and the child offsets.
- The bbox should satisfy ``x0 <= x1 and y0 <= y1``.
- Parameters
- ----------
- renderer : `.RendererBase` subclass
- Returns
- -------
- bbox
- list of (xoffset, yoffset) pairs
- """
- raise NotImplementedError(
- "get_bbox_and_offsets must be overridden in derived classes")
- def get_bbox(self, renderer):
- """Return the bbox of the offsetbox, ignoring parent offsets."""
- bbox, offsets = self._get_bbox_and_child_offsets(renderer)
- return bbox
- def get_window_extent(self, renderer=None):
- # docstring inherited
- if renderer is None:
- renderer = self.get_figure(root=True)._get_renderer()
- bbox = self.get_bbox(renderer)
- try: # Some subclasses redefine get_offset to take no args.
- px, py = self.get_offset(bbox, renderer)
- except TypeError:
- px, py = self.get_offset()
- return bbox.translated(px, py)
- def draw(self, renderer):
- """
- Update the location of children if necessary and draw them
- to the given *renderer*.
- """
- bbox, offsets = self._get_bbox_and_child_offsets(renderer)
- px, py = self.get_offset(bbox, renderer)
- for c, (ox, oy) in zip(self.get_visible_children(), offsets):
- c.set_offset((px + ox, py + oy))
- c.draw(renderer)
- _bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
- self.stale = False
- class PackerBase(OffsetBox):
- def __init__(self, pad=0., sep=0., width=None, height=None,
- align="baseline", mode="fixed", children=None):
- """
- Parameters
- ----------
- pad : float, default: 0.0
- The boundary padding in points.
- sep : float, default: 0.0
- The spacing between items in points.
- width, height : float, optional
- Width and height of the container box in pixels, calculated if
- *None*.
- align : {'top', 'bottom', 'left', 'right', 'center', 'baseline'}, \
- default: 'baseline'
- Alignment of boxes.
- mode : {'fixed', 'expand', 'equal'}, default: 'fixed'
- The packing mode.
- - 'fixed' packs the given `.Artist`\\s tight with *sep* spacing.
- - 'expand' uses the maximal available space to distribute the
- artists with equal spacing in between.
- - 'equal': Each artist an equal fraction of the available space
- and is left-aligned (or top-aligned) therein.
- children : list of `.Artist`
- The artists to pack.
- Notes
- -----
- *pad* and *sep* are in points and will be scaled with the renderer
- dpi, while *width* and *height* are in pixels.
- """
- super().__init__()
- self.height = height
- self.width = width
- self.sep = sep
- self.pad = pad
- self.mode = mode
- self.align = align
- self._children = children
- class VPacker(PackerBase):
- """
- VPacker packs its children vertically, automatically adjusting their
- relative positions at draw time.
- .. code-block:: none
- +---------+
- | Child 1 |
- | Child 2 |
- | Child 3 |
- +---------+
- """
- def _get_bbox_and_child_offsets(self, renderer):
- # docstring inherited
- dpicor = renderer.points_to_pixels(1.)
- pad = self.pad * dpicor
- sep = self.sep * dpicor
- if self.width is not None:
- for c in self.get_visible_children():
- if isinstance(c, PackerBase) and c.mode == "expand":
- c.set_width(self.width)
- bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
- (x0, x1), xoffsets = _get_aligned_offsets(
- [bbox.intervalx for bbox in bboxes], self.width, self.align)
- height, yoffsets = _get_packed_offsets(
- [bbox.height for bbox in bboxes], self.height, sep, self.mode)
- yoffsets = height - (yoffsets + [bbox.y1 for bbox in bboxes])
- ydescent = yoffsets[0]
- yoffsets = yoffsets - ydescent
- return (
- Bbox.from_bounds(x0, -ydescent, x1 - x0, height).padded(pad),
- [*zip(xoffsets, yoffsets)])
- class HPacker(PackerBase):
- """
- HPacker packs its children horizontally, automatically adjusting their
- relative positions at draw time.
- .. code-block:: none
- +-------------------------------+
- | Child 1 Child 2 Child 3 |
- +-------------------------------+
- """
- def _get_bbox_and_child_offsets(self, renderer):
- # docstring inherited
- dpicor = renderer.points_to_pixels(1.)
- pad = self.pad * dpicor
- sep = self.sep * dpicor
- bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()]
- if not bboxes:
- return Bbox.from_bounds(0, 0, 0, 0).padded(pad), []
- (y0, y1), yoffsets = _get_aligned_offsets(
- [bbox.intervaly for bbox in bboxes], self.height, self.align)
- width, xoffsets = _get_packed_offsets(
- [bbox.width for bbox in bboxes], self.width, sep, self.mode)
- x0 = bboxes[0].x0
- xoffsets -= ([bbox.x0 for bbox in bboxes] - x0)
- return (Bbox.from_bounds(x0, y0, width, y1 - y0).padded(pad),
- [*zip(xoffsets, yoffsets)])
- class PaddedBox(OffsetBox):
- """
- A container to add a padding around an `.Artist`.
- The `.PaddedBox` contains a `.FancyBboxPatch` that is used to visualize
- it when rendering.
- .. code-block:: none
- +----------------------------+
- | |
- | |
- | |
- | <--pad--> Artist |
- | ^ |
- | pad |
- | v |
- +----------------------------+
- Attributes
- ----------
- pad : float
- The padding in points.
- patch : `.FancyBboxPatch`
- When *draw_frame* is True, this `.FancyBboxPatch` is made visible and
- creates a border around the box.
- """
- def __init__(self, child, pad=0., *, draw_frame=False, patch_attrs=None):
- """
- Parameters
- ----------
- child : `~matplotlib.artist.Artist`
- The contained `.Artist`.
- pad : float, default: 0.0
- The padding in points. This will be scaled with the renderer dpi.
- In contrast, *width* and *height* are in *pixels* and thus not
- scaled.
- draw_frame : bool
- Whether to draw the contained `.FancyBboxPatch`.
- patch_attrs : dict or None
- Additional parameters passed to the contained `.FancyBboxPatch`.
- """
- super().__init__()
- self.pad = pad
- self._children = [child]
- self.patch = FancyBboxPatch(
- xy=(0.0, 0.0), width=1., height=1.,
- facecolor='w', edgecolor='k',
- mutation_scale=1, # self.prop.get_size_in_points(),
- snap=True,
- visible=draw_frame,
- boxstyle="square,pad=0",
- )
- if patch_attrs is not None:
- self.patch.update(patch_attrs)
- def _get_bbox_and_child_offsets(self, renderer):
- # docstring inherited.
- pad = self.pad * renderer.points_to_pixels(1.)
- return (self._children[0].get_bbox(renderer).padded(pad), [(0, 0)])
- def draw(self, renderer):
- # docstring inherited
- bbox, offsets = self._get_bbox_and_child_offsets(renderer)
- px, py = self.get_offset(bbox, renderer)
- for c, (ox, oy) in zip(self.get_visible_children(), offsets):
- c.set_offset((px + ox, py + oy))
- self.draw_frame(renderer)
- for c in self.get_visible_children():
- c.draw(renderer)
- self.stale = False
- def update_frame(self, bbox, fontsize=None):
- self.patch.set_bounds(bbox.bounds)
- if fontsize:
- self.patch.set_mutation_scale(fontsize)
- self.stale = True
- def draw_frame(self, renderer):
- # update the location and size of the legend
- self.update_frame(self.get_window_extent(renderer))
- self.patch.draw(renderer)
- class DrawingArea(OffsetBox):
- """
- The DrawingArea can contain any Artist as a child. The DrawingArea
- has a fixed width and height. The position of children relative to
- the parent is fixed. The children can be clipped at the
- boundaries of the parent.
- """
- def __init__(self, width, height, xdescent=0., ydescent=0., clip=False):
- """
- Parameters
- ----------
- width, height : float
- Width and height of the container box.
- xdescent, ydescent : float
- Descent of the box in x- and y-direction.
- clip : bool
- Whether to clip the children to the box.
- """
- super().__init__()
- self.width = width
- self.height = height
- self.xdescent = xdescent
- self.ydescent = ydescent
- self._clip_children = clip
- self.offset_transform = mtransforms.Affine2D()
- self.dpi_transform = mtransforms.Affine2D()
- @property
- def clip_children(self):
- """
- If the children of this DrawingArea should be clipped
- by DrawingArea bounding box.
- """
- return self._clip_children
- @clip_children.setter
- def clip_children(self, val):
- self._clip_children = bool(val)
- self.stale = True
- def get_transform(self):
- """
- Return the `~matplotlib.transforms.Transform` applied to the children.
- """
- return self.dpi_transform + self.offset_transform
- def set_transform(self, t):
- """
- set_transform is ignored.
- """
- def set_offset(self, xy):
- """
- Set the offset of the container.
- Parameters
- ----------
- xy : (float, float)
- The (x, y) coordinates of the offset in display units.
- """
- self._offset = xy
- self.offset_transform.clear()
- self.offset_transform.translate(xy[0], xy[1])
- self.stale = True
- def get_offset(self):
- """Return offset of the container."""
- return self._offset
- def get_bbox(self, renderer):
- # docstring inherited
- dpi_cor = renderer.points_to_pixels(1.)
- return Bbox.from_bounds(
- -self.xdescent * dpi_cor, -self.ydescent * dpi_cor,
- self.width * dpi_cor, self.height * dpi_cor)
- def add_artist(self, a):
- """Add an `.Artist` to the container box."""
- self._children.append(a)
- if not a.is_transform_set():
- a.set_transform(self.get_transform())
- if self.axes is not None:
- a.axes = self.axes
- fig = self.get_figure(root=False)
- if fig is not None:
- a.set_figure(fig)
- def draw(self, renderer):
- # docstring inherited
- dpi_cor = renderer.points_to_pixels(1.)
- self.dpi_transform.clear()
- self.dpi_transform.scale(dpi_cor)
- # At this point the DrawingArea has a transform
- # to the display space so the path created is
- # good for clipping children
- tpath = mtransforms.TransformedPath(
- mpath.Path([[0, 0], [0, self.height],
- [self.width, self.height],
- [self.width, 0]]),
- self.get_transform())
- for c in self._children:
- if self._clip_children and not (c.clipbox or c._clippath):
- c.set_clip_path(tpath)
- c.draw(renderer)
- _bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
- self.stale = False
- class TextArea(OffsetBox):
- """
- The TextArea is a container artist for a single Text instance.
- The text is placed at (0, 0) with baseline+left alignment, by default. The
- width and height of the TextArea instance is the width and height of its
- child text.
- """
- def __init__(self, s,
- *,
- textprops=None,
- multilinebaseline=False,
- ):
- """
- Parameters
- ----------
- s : str
- The text to be displayed.
- textprops : dict, default: {}
- Dictionary of keyword parameters to be passed to the `.Text`
- instance in the TextArea.
- multilinebaseline : bool, default: False
- Whether the baseline for multiline text is adjusted so that it
- is (approximately) center-aligned with single-line text.
- """
- if textprops is None:
- textprops = {}
- self._text = mtext.Text(0, 0, s, **textprops)
- super().__init__()
- self._children = [self._text]
- self.offset_transform = mtransforms.Affine2D()
- self._baseline_transform = mtransforms.Affine2D()
- self._text.set_transform(self.offset_transform +
- self._baseline_transform)
- self._multilinebaseline = multilinebaseline
- def set_text(self, s):
- """Set the text of this area as a string."""
- self._text.set_text(s)
- self.stale = True
- def get_text(self):
- """Return the string representation of this area's text."""
- return self._text.get_text()
- def set_multilinebaseline(self, t):
- """
- Set multilinebaseline.
- If True, the baseline for multiline text is adjusted so that it is
- (approximately) center-aligned with single-line text. This is used
- e.g. by the legend implementation so that single-line labels are
- baseline-aligned, but multiline labels are "center"-aligned with them.
- """
- self._multilinebaseline = t
- self.stale = True
- def get_multilinebaseline(self):
- """
- Get multilinebaseline.
- """
- return self._multilinebaseline
- def set_transform(self, t):
- """
- set_transform is ignored.
- """
- def set_offset(self, xy):
- """
- Set the offset of the container.
- Parameters
- ----------
- xy : (float, float)
- The (x, y) coordinates of the offset in display units.
- """
- self._offset = xy
- self.offset_transform.clear()
- self.offset_transform.translate(xy[0], xy[1])
- self.stale = True
- def get_offset(self):
- """Return offset of the container."""
- return self._offset
- def get_bbox(self, renderer):
- _, h_, d_ = mtext._get_text_metrics_with_cache(
- renderer, "lp", self._text._fontproperties,
- ismath="TeX" if self._text.get_usetex() else False,
- dpi=self.get_figure(root=True).dpi)
- bbox, info, yd = self._text._get_layout(renderer)
- w, h = bbox.size
- self._baseline_transform.clear()
- if len(info) > 1 and self._multilinebaseline:
- yd_new = 0.5 * h - 0.5 * (h_ - d_)
- self._baseline_transform.translate(0, yd - yd_new)
- yd = yd_new
- else: # single line
- h_d = max(h_ - d_, h - yd)
- h = h_d + yd
- ha = self._text.get_horizontalalignment()
- x0 = {"left": 0, "center": -w / 2, "right": -w}[ha]
- return Bbox.from_bounds(x0, -yd, w, h)
- def draw(self, renderer):
- # docstring inherited
- self._text.draw(renderer)
- _bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
- self.stale = False
- class AuxTransformBox(OffsetBox):
- """
- An OffsetBox with an auxiliary transform.
- All child artists are first transformed with *aux_transform*, then
- translated with an offset (the same for all children) so the bounding
- box of the children matches the drawn box. (In other words, adding an
- arbitrary translation to *aux_transform* has no effect as it will be
- cancelled out by the later offsetting.)
- `AuxTransformBox` is similar to `.DrawingArea`, except that the extent of
- the box is not predetermined but calculated from the window extent of its
- children, and the extent of the children will be calculated in the
- transformed coordinate.
- """
- def __init__(self, aux_transform):
- self.aux_transform = aux_transform
- super().__init__()
- self.offset_transform = mtransforms.Affine2D()
- # ref_offset_transform makes offset_transform always relative to the
- # lower-left corner of the bbox of its children.
- self.ref_offset_transform = mtransforms.Affine2D()
- def add_artist(self, a):
- """Add an `.Artist` to the container box."""
- self._children.append(a)
- a.set_transform(self.get_transform())
- self.stale = True
- def get_transform(self):
- """Return the `.Transform` applied to the children."""
- return (self.aux_transform
- + self.ref_offset_transform
- + self.offset_transform)
- def set_transform(self, t):
- """
- set_transform is ignored.
- """
- def set_offset(self, xy):
- """
- Set the offset of the container.
- Parameters
- ----------
- xy : (float, float)
- The (x, y) coordinates of the offset in display units.
- """
- self._offset = xy
- self.offset_transform.clear()
- self.offset_transform.translate(xy[0], xy[1])
- self.stale = True
- def get_offset(self):
- """Return offset of the container."""
- return self._offset
- def get_bbox(self, renderer):
- # clear the offset transforms
- _off = self.offset_transform.get_matrix() # to be restored later
- self.ref_offset_transform.clear()
- self.offset_transform.clear()
- # calculate the extent
- bboxes = [c.get_window_extent(renderer) for c in self._children]
- ub = Bbox.union(bboxes)
- # adjust ref_offset_transform
- self.ref_offset_transform.translate(-ub.x0, -ub.y0)
- # restore offset transform
- self.offset_transform.set_matrix(_off)
- return Bbox.from_bounds(0, 0, ub.width, ub.height)
- def draw(self, renderer):
- # docstring inherited
- for c in self._children:
- c.draw(renderer)
- _bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
- self.stale = False
- class AnchoredOffsetbox(OffsetBox):
- """
- An OffsetBox placed according to location *loc*.
- AnchoredOffsetbox has a single child. When multiple children are needed,
- use an extra OffsetBox to enclose them. By default, the offset box is
- anchored against its parent Axes. You may explicitly specify the
- *bbox_to_anchor*.
- """
- zorder = 5 # zorder of the legend
- # Location codes
- codes = {'upper right': 1,
- 'upper left': 2,
- 'lower left': 3,
- 'lower right': 4,
- 'right': 5,
- 'center left': 6,
- 'center right': 7,
- 'lower center': 8,
- 'upper center': 9,
- 'center': 10,
- }
- def __init__(self, loc, *,
- pad=0.4, borderpad=0.5,
- child=None, prop=None, frameon=True,
- bbox_to_anchor=None,
- bbox_transform=None,
- **kwargs):
- """
- Parameters
- ----------
- loc : str
- The box location. Valid locations are
- 'upper left', 'upper center', 'upper right',
- 'center left', 'center', 'center right',
- 'lower left', 'lower center', 'lower right'.
- For backward compatibility, numeric values are accepted as well.
- See the parameter *loc* of `.Legend` for details.
- pad : float, default: 0.4
- Padding around the child as fraction of the fontsize.
- borderpad : float, default: 0.5
- Padding between the offsetbox frame and the *bbox_to_anchor*.
- child : `.OffsetBox`
- The box that will be anchored.
- prop : `.FontProperties`
- This is only used as a reference for paddings. If not given,
- :rc:`legend.fontsize` is used.
- frameon : bool
- Whether to draw a frame around the box.
- bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats
- Box that is used to position the legend in conjunction with *loc*.
- bbox_transform : None or :class:`matplotlib.transforms.Transform`
- The transform for the bounding box (*bbox_to_anchor*).
- **kwargs
- All other parameters are passed on to `.OffsetBox`.
- Notes
- -----
- See `.Legend` for a detailed description of the anchoring mechanism.
- """
- super().__init__(**kwargs)
- self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform)
- self.set_child(child)
- if isinstance(loc, str):
- loc = _api.check_getitem(self.codes, loc=loc)
- self.loc = loc
- self.borderpad = borderpad
- self.pad = pad
- if prop is None:
- self.prop = FontProperties(size=mpl.rcParams["legend.fontsize"])
- else:
- self.prop = FontProperties._from_any(prop)
- if isinstance(prop, dict) and "size" not in prop:
- self.prop.set_size(mpl.rcParams["legend.fontsize"])
- self.patch = FancyBboxPatch(
- xy=(0.0, 0.0), width=1., height=1.,
- facecolor='w', edgecolor='k',
- mutation_scale=self.prop.get_size_in_points(),
- snap=True,
- visible=frameon,
- boxstyle="square,pad=0",
- )
- def set_child(self, child):
- """Set the child to be anchored."""
- self._child = child
- if child is not None:
- child.axes = self.axes
- self.stale = True
- def get_child(self):
- """Return the child."""
- return self._child
- def get_children(self):
- """Return the list of children."""
- return [self._child]
- def get_bbox(self, renderer):
- # docstring inherited
- fontsize = renderer.points_to_pixels(self.prop.get_size_in_points())
- pad = self.pad * fontsize
- return self.get_child().get_bbox(renderer).padded(pad)
- def get_bbox_to_anchor(self):
- """Return the bbox that the box is anchored to."""
- if self._bbox_to_anchor is None:
- return self.axes.bbox
- else:
- transform = self._bbox_to_anchor_transform
- if transform is None:
- return self._bbox_to_anchor
- else:
- return TransformedBbox(self._bbox_to_anchor, transform)
- def set_bbox_to_anchor(self, bbox, transform=None):
- """
- Set the bbox that the box is anchored to.
- *bbox* can be a Bbox instance, a list of [left, bottom, width,
- height], or a list of [left, bottom] where the width and
- height will be assumed to be zero. The bbox will be
- transformed to display coordinate by the given transform.
- """
- if bbox is None or isinstance(bbox, BboxBase):
- self._bbox_to_anchor = bbox
- else:
- try:
- l = len(bbox)
- except TypeError as err:
- raise ValueError(f"Invalid bbox: {bbox}") from err
- if l == 2:
- bbox = [bbox[0], bbox[1], 0, 0]
- self._bbox_to_anchor = Bbox.from_bounds(*bbox)
- self._bbox_to_anchor_transform = transform
- self.stale = True
- @_compat_get_offset
- def get_offset(self, bbox, renderer):
- # docstring inherited
- pad = (self.borderpad
- * renderer.points_to_pixels(self.prop.get_size_in_points()))
- bbox_to_anchor = self.get_bbox_to_anchor()
- x0, y0 = _get_anchored_bbox(
- self.loc, Bbox.from_bounds(0, 0, bbox.width, bbox.height),
- bbox_to_anchor, pad)
- return x0 - bbox.x0, y0 - bbox.y0
- def update_frame(self, bbox, fontsize=None):
- self.patch.set_bounds(bbox.bounds)
- if fontsize:
- self.patch.set_mutation_scale(fontsize)
- def draw(self, renderer):
- # docstring inherited
- if not self.get_visible():
- return
- # update the location and size of the legend
- bbox = self.get_window_extent(renderer)
- fontsize = renderer.points_to_pixels(self.prop.get_size_in_points())
- self.update_frame(bbox, fontsize)
- self.patch.draw(renderer)
- px, py = self.get_offset(self.get_bbox(renderer), renderer)
- self.get_child().set_offset((px, py))
- self.get_child().draw(renderer)
- self.stale = False
- def _get_anchored_bbox(loc, bbox, parentbbox, borderpad):
- """
- Return the (x, y) position of the *bbox* anchored at the *parentbbox* with
- the *loc* code with the *borderpad*.
- """
- # This is only called internally and *loc* should already have been
- # validated. If 0 (None), we just let ``bbox.anchored`` raise.
- c = [None, "NE", "NW", "SW", "SE", "E", "W", "E", "S", "N", "C"][loc]
- container = parentbbox.padded(-borderpad)
- return bbox.anchored(c, container=container).p0
- class AnchoredText(AnchoredOffsetbox):
- """
- AnchoredOffsetbox with Text.
- """
- def __init__(self, s, loc, *, pad=0.4, borderpad=0.5, prop=None, **kwargs):
- """
- Parameters
- ----------
- s : str
- Text.
- loc : str
- Location code. See `AnchoredOffsetbox`.
- pad : float, default: 0.4
- Padding around the text as fraction of the fontsize.
- borderpad : float, default: 0.5
- Spacing between the offsetbox frame and the *bbox_to_anchor*.
- prop : dict, optional
- Dictionary of keyword parameters to be passed to the
- `~matplotlib.text.Text` instance contained inside AnchoredText.
- **kwargs
- All other parameters are passed to `AnchoredOffsetbox`.
- """
- if prop is None:
- prop = {}
- badkwargs = {'va', 'verticalalignment'}
- if badkwargs & set(prop):
- raise ValueError(
- 'Mixing verticalalignment with AnchoredText is not supported.')
- self.txt = TextArea(s, textprops=prop)
- fp = self.txt._text.get_fontproperties()
- super().__init__(
- loc, pad=pad, borderpad=borderpad, child=self.txt, prop=fp,
- **kwargs)
- class OffsetImage(OffsetBox):
- def __init__(self, arr, *,
- zoom=1,
- cmap=None,
- norm=None,
- interpolation=None,
- origin=None,
- filternorm=True,
- filterrad=4.0,
- resample=False,
- dpi_cor=True,
- **kwargs
- ):
- super().__init__()
- self._dpi_cor = dpi_cor
- self.image = BboxImage(bbox=self.get_window_extent,
- cmap=cmap,
- norm=norm,
- interpolation=interpolation,
- origin=origin,
- filternorm=filternorm,
- filterrad=filterrad,
- resample=resample,
- **kwargs
- )
- self._children = [self.image]
- self.set_zoom(zoom)
- self.set_data(arr)
- def set_data(self, arr):
- self._data = np.asarray(arr)
- self.image.set_data(self._data)
- self.stale = True
- def get_data(self):
- return self._data
- def set_zoom(self, zoom):
- self._zoom = zoom
- self.stale = True
- def get_zoom(self):
- return self._zoom
- def get_offset(self):
- """Return offset of the container."""
- return self._offset
- def get_children(self):
- return [self.image]
- def get_bbox(self, renderer):
- dpi_cor = renderer.points_to_pixels(1.) if self._dpi_cor else 1.
- zoom = self.get_zoom()
- data = self.get_data()
- ny, nx = data.shape[:2]
- w, h = dpi_cor * nx * zoom, dpi_cor * ny * zoom
- return Bbox.from_bounds(0, 0, w, h)
- def draw(self, renderer):
- # docstring inherited
- self.image.draw(renderer)
- # bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
- self.stale = False
- class AnnotationBbox(martist.Artist, mtext._AnnotationBase):
- """
- Container for an `OffsetBox` referring to a specific position *xy*.
- Optionally an arrow pointing from the offsetbox to *xy* can be drawn.
- This is like `.Annotation`, but with `OffsetBox` instead of `.Text`.
- """
- zorder = 3
- def __str__(self):
- return f"AnnotationBbox({self.xy[0]:g},{self.xy[1]:g})"
- @_docstring.interpd
- def __init__(self, offsetbox, xy, xybox=None, xycoords='data', boxcoords=None, *,
- frameon=True, pad=0.4, # FancyBboxPatch boxstyle.
- annotation_clip=None,
- box_alignment=(0.5, 0.5),
- bboxprops=None,
- arrowprops=None,
- fontsize=None,
- **kwargs):
- """
- Parameters
- ----------
- offsetbox : `OffsetBox`
- xy : (float, float)
- The point *(x, y)* to annotate. The coordinate system is determined
- by *xycoords*.
- xybox : (float, float), default: *xy*
- The position *(x, y)* to place the text at. The coordinate system
- is determined by *boxcoords*.
- xycoords : single or two-tuple of str or `.Artist` or `.Transform` or \
- callable, default: 'data'
- The coordinate system that *xy* is given in. See the parameter
- *xycoords* in `.Annotation` for a detailed description.
- boxcoords : single or two-tuple of str or `.Artist` or `.Transform` \
- or callable, default: value of *xycoords*
- The coordinate system that *xybox* is given in. See the parameter
- *textcoords* in `.Annotation` for a detailed description.
- frameon : bool, default: True
- By default, the text is surrounded by a white `.FancyBboxPatch`
- (accessible as the ``patch`` attribute of the `.AnnotationBbox`).
- If *frameon* is set to False, this patch is made invisible.
- annotation_clip: bool or None, default: None
- Whether to clip (i.e. not draw) the annotation when the annotation
- point *xy* is outside the Axes area.
- - If *True*, the annotation will be clipped when *xy* is outside
- the Axes.
- - If *False*, the annotation will always be drawn.
- - If *None*, the annotation will be clipped when *xy* is outside
- the Axes and *xycoords* is 'data'.
- pad : float, default: 0.4
- Padding around the offsetbox.
- box_alignment : (float, float)
- A tuple of two floats for a vertical and horizontal alignment of
- the offset box w.r.t. the *boxcoords*.
- The lower-left corner is (0, 0) and upper-right corner is (1, 1).
- bboxprops : dict, optional
- A dictionary of properties to set for the annotation bounding box,
- for example *boxstyle* and *alpha*. See `.FancyBboxPatch` for
- details.
- arrowprops: dict, optional
- Arrow properties, see `.Annotation` for description.
- fontsize: float or str, optional
- Translated to points and passed as *mutation_scale* into
- `.FancyBboxPatch` to scale attributes of the box style (e.g. pad
- or rounding_size). The name is chosen in analogy to `.Text` where
- *fontsize* defines the mutation scale as well. If not given,
- :rc:`legend.fontsize` is used. See `.Text.set_fontsize` for valid
- values.
- **kwargs
- Other `AnnotationBbox` properties. See `.AnnotationBbox.set` for
- a list.
- """
- martist.Artist.__init__(self)
- mtext._AnnotationBase.__init__(
- self, xy, xycoords=xycoords, annotation_clip=annotation_clip)
- self.offsetbox = offsetbox
- self.arrowprops = arrowprops.copy() if arrowprops is not None else None
- self.set_fontsize(fontsize)
- self.xybox = xybox if xybox is not None else xy
- self.boxcoords = boxcoords if boxcoords is not None else xycoords
- self._box_alignment = box_alignment
- if arrowprops is not None:
- self._arrow_relpos = self.arrowprops.pop("relpos", (0.5, 0.5))
- self.arrow_patch = FancyArrowPatch((0, 0), (1, 1),
- **self.arrowprops)
- else:
- self._arrow_relpos = None
- self.arrow_patch = None
- self.patch = FancyBboxPatch( # frame
- xy=(0.0, 0.0), width=1., height=1.,
- facecolor='w', edgecolor='k',
- mutation_scale=self.prop.get_size_in_points(),
- snap=True,
- visible=frameon,
- )
- self.patch.set_boxstyle("square", pad=pad)
- if bboxprops:
- self.patch.set(**bboxprops)
- self._internal_update(kwargs)
- @property
- def xyann(self):
- return self.xybox
- @xyann.setter
- def xyann(self, xyann):
- self.xybox = xyann
- self.stale = True
- @property
- def anncoords(self):
- return self.boxcoords
- @anncoords.setter
- def anncoords(self, coords):
- self.boxcoords = coords
- self.stale = True
- def contains(self, mouseevent):
- if self._different_canvas(mouseevent):
- return False, {}
- if not self._check_xy(None):
- return False, {}
- return self.offsetbox.contains(mouseevent)
- # self.arrow_patch is currently not checked as this can be a line - JJ
- def get_children(self):
- children = [self.offsetbox, self.patch]
- if self.arrow_patch:
- children.append(self.arrow_patch)
- return children
- def set_figure(self, fig):
- if self.arrow_patch is not None:
- self.arrow_patch.set_figure(fig)
- self.offsetbox.set_figure(fig)
- martist.Artist.set_figure(self, fig)
- def set_fontsize(self, s=None):
- """
- Set the fontsize in points.
- If *s* is not given, reset to :rc:`legend.fontsize`.
- """
- if s is None:
- s = mpl.rcParams["legend.fontsize"]
- self.prop = FontProperties(size=s)
- self.stale = True
- def get_fontsize(self):
- """Return the fontsize in points."""
- return self.prop.get_size_in_points()
- def get_window_extent(self, renderer=None):
- # docstring inherited
- if renderer is None:
- renderer = self.get_figure(root=True)._get_renderer()
- self.update_positions(renderer)
- return Bbox.union([child.get_window_extent(renderer)
- for child in self.get_children()])
- def get_tightbbox(self, renderer=None):
- # docstring inherited
- if renderer is None:
- renderer = self.get_figure(root=True)._get_renderer()
- self.update_positions(renderer)
- return Bbox.union([child.get_tightbbox(renderer)
- for child in self.get_children()])
- def update_positions(self, renderer):
- """Update pixel positions for the annotated point, the text, and the arrow."""
- ox0, oy0 = self._get_xy(renderer, self.xybox, self.boxcoords)
- bbox = self.offsetbox.get_bbox(renderer)
- fw, fh = self._box_alignment
- self.offsetbox.set_offset(
- (ox0 - fw*bbox.width - bbox.x0, oy0 - fh*bbox.height - bbox.y0))
- bbox = self.offsetbox.get_window_extent(renderer)
- self.patch.set_bounds(bbox.bounds)
- mutation_scale = renderer.points_to_pixels(self.get_fontsize())
- self.patch.set_mutation_scale(mutation_scale)
- if self.arrowprops:
- # Use FancyArrowPatch if self.arrowprops has "arrowstyle" key.
- # Adjust the starting point of the arrow relative to the textbox.
- # TODO: Rotation needs to be accounted.
- arrow_begin = bbox.p0 + bbox.size * self._arrow_relpos
- arrow_end = self._get_position_xy(renderer)
- # The arrow (from arrow_begin to arrow_end) will be first clipped
- # by patchA and patchB, then shrunk by shrinkA and shrinkB (in
- # points). If patch A is not set, self.bbox_patch is used.
- self.arrow_patch.set_positions(arrow_begin, arrow_end)
- if "mutation_scale" in self.arrowprops:
- mutation_scale = renderer.points_to_pixels(
- self.arrowprops["mutation_scale"])
- # Else, use fontsize-based mutation_scale defined above.
- self.arrow_patch.set_mutation_scale(mutation_scale)
- patchA = self.arrowprops.get("patchA", self.patch)
- self.arrow_patch.set_patchA(patchA)
- def draw(self, renderer):
- # docstring inherited
- if not self.get_visible() or not self._check_xy(renderer):
- return
- renderer.open_group(self.__class__.__name__, gid=self.get_gid())
- self.update_positions(renderer)
- if self.arrow_patch is not None:
- if (self.arrow_patch.get_figure(root=False) is None and
- (fig := self.get_figure(root=False)) is not None):
- self.arrow_patch.set_figure(fig)
- self.arrow_patch.draw(renderer)
- self.patch.draw(renderer)
- self.offsetbox.draw(renderer)
- renderer.close_group(self.__class__.__name__)
- self.stale = False
- class DraggableBase:
- """
- Helper base class for a draggable artist (legend, offsetbox).
- Derived classes must override the following methods::
- def save_offset(self):
- '''
- Called when the object is picked for dragging; should save the
- reference position of the artist.
- '''
- def update_offset(self, dx, dy):
- '''
- Called during the dragging; (*dx*, *dy*) is the pixel offset from
- the point where the mouse drag started.
- '''
- Optionally, you may override the following method::
- def finalize_offset(self):
- '''Called when the mouse is released.'''
- In the current implementation of `.DraggableLegend` and
- `DraggableAnnotation`, `update_offset` places the artists in display
- coordinates, and `finalize_offset` recalculates their position in axes
- coordinate and set a relevant attribute.
- """
- def __init__(self, ref_artist, use_blit=False):
- self.ref_artist = ref_artist
- if not ref_artist.pickable():
- ref_artist.set_picker(self._picker)
- self.got_artist = False
- self._use_blit = use_blit and self.canvas.supports_blit
- callbacks = self.canvas.callbacks
- self._disconnectors = [
- functools.partial(
- callbacks.disconnect, callbacks._connect_picklable(name, func))
- for name, func in [
- ("pick_event", self.on_pick),
- ("button_release_event", self.on_release),
- ("motion_notify_event", self.on_motion),
- ]
- ]
- @staticmethod
- def _picker(artist, mouseevent):
- # A custom picker to prevent dragging on mouse scroll events
- if mouseevent.name == "scroll_event":
- return False, {}
- return artist.contains(mouseevent)
- # A property, not an attribute, to maintain picklability.
- canvas = property(lambda self: self.ref_artist.get_figure(root=True).canvas)
- cids = property(lambda self: [
- disconnect.args[0] for disconnect in self._disconnectors[:2]])
- def on_motion(self, evt):
- if self._check_still_parented() and self.got_artist:
- dx = evt.x - self.mouse_x
- dy = evt.y - self.mouse_y
- self.update_offset(dx, dy)
- if self._use_blit:
- self.canvas.restore_region(self.background)
- self.ref_artist.draw(
- self.ref_artist.get_figure(root=True)._get_renderer())
- self.canvas.blit()
- else:
- self.canvas.draw()
- def on_pick(self, evt):
- if self._check_still_parented():
- if evt.artist == self.ref_artist:
- self.mouse_x = evt.mouseevent.x
- self.mouse_y = evt.mouseevent.y
- self.save_offset()
- self.got_artist = True
- if self.got_artist and self._use_blit:
- self.ref_artist.set_animated(True)
- self.canvas.draw()
- fig = self.ref_artist.get_figure(root=False)
- self.background = self.canvas.copy_from_bbox(fig.bbox)
- self.ref_artist.draw(fig._get_renderer())
- self.canvas.blit()
- def on_release(self, event):
- if self._check_still_parented() and self.got_artist:
- self.finalize_offset()
- self.got_artist = False
- if self._use_blit:
- self.canvas.restore_region(self.background)
- self.ref_artist.draw(self.ref_artist.figure._get_renderer())
- self.canvas.blit()
- self.ref_artist.set_animated(False)
- def _check_still_parented(self):
- if self.ref_artist.get_figure(root=False) is None:
- self.disconnect()
- return False
- else:
- return True
- def disconnect(self):
- """Disconnect the callbacks."""
- for disconnector in self._disconnectors:
- disconnector()
- def save_offset(self):
- pass
- def update_offset(self, dx, dy):
- pass
- def finalize_offset(self):
- pass
- class DraggableOffsetBox(DraggableBase):
- def __init__(self, ref_artist, offsetbox, use_blit=False):
- super().__init__(ref_artist, use_blit=use_blit)
- self.offsetbox = offsetbox
- def save_offset(self):
- offsetbox = self.offsetbox
- renderer = offsetbox.get_figure(root=True)._get_renderer()
- offset = offsetbox.get_offset(offsetbox.get_bbox(renderer), renderer)
- self.offsetbox_x, self.offsetbox_y = offset
- self.offsetbox.set_offset(offset)
- def update_offset(self, dx, dy):
- loc_in_canvas = self.offsetbox_x + dx, self.offsetbox_y + dy
- self.offsetbox.set_offset(loc_in_canvas)
- def get_loc_in_canvas(self):
- offsetbox = self.offsetbox
- renderer = offsetbox.get_figure(root=True)._get_renderer()
- bbox = offsetbox.get_bbox(renderer)
- ox, oy = offsetbox._offset
- loc_in_canvas = (ox + bbox.x0, oy + bbox.y0)
- return loc_in_canvas
- class DraggableAnnotation(DraggableBase):
- def __init__(self, annotation, use_blit=False):
- super().__init__(annotation, use_blit=use_blit)
- self.annotation = annotation
- def save_offset(self):
- ann = self.annotation
- self.ox, self.oy = ann.get_transform().transform(ann.xyann)
- def update_offset(self, dx, dy):
- ann = self.annotation
- ann.xyann = ann.get_transform().inverted().transform(
- (self.ox + dx, self.oy + dy))
|