123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821 |
- 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 numpy as np
- from matplotlib import cbook, docstring, rcParams
- 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
- # for debugging use
- def bbox_artist(*args, **kwargs):
- if DEBUG:
- mbbox_artist(*args, **kwargs)
- # _get_packed_offsets() and _get_aligned_offsets() are coded assuming
- # that we are packing boxes horizontally. But same function will be
- # used with vertical packing.
- def _get_packed_offsets(wd_list, total, sep, mode="fixed"):
- """
- Given a list of (width, xdescent) of each boxes, calculate the
- total width and the x-offset positions of each items according to
- *mode*. xdescent is analogous to the usual descent, but along the
- x-direction. xdescent values are currently ignored.
- 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 modes:
- - '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
- ----------
- wd_list : list of (float, float)
- (width, xdescent) of boxes to be packed.
- total : float or None
- Intended total length. *None* if not used.
- sep : float
- 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.
- """
- w_list, d_list = zip(*wd_list) # d_list is currently not used.
- cbook._check_in_list(["fixed", "expand", "equal"], mode=mode)
- if mode == "fixed":
- offsets_ = np.cumsum([0] + [w + sep for w in w_list])
- 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(w_list) > 1:
- sep = (total - sum(w_list)) / (len(w_list) - 1)
- else:
- sep = 0
- offsets_ = np.cumsum([0] + [w + sep for w in w_list])
- offsets = offsets_[:-1]
- return total, offsets
- elif mode == "equal":
- maxh = max(w_list)
- 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(w_list)
- else:
- sep = total / len(w_list) - maxh
- offsets = (maxh + sep) * np.arange(len(w_list))
- return total, offsets
- def _get_aligned_offsets(hd_list, height, align="baseline"):
- """
- Given a list of (height, descent) of each boxes, align the boxes
- with *align* and calculate the y-offsets of each boxes.
- total width and the offset positions of each items according to
- *mode*. xdescent is analogous to the usual descent, but along the
- x-direction. xdescent values are currently ignored.
- Parameters
- ----------
- hd_list
- List of (height, xdescent) of boxes to be aligned.
- height : float or None
- Intended total length. If None, the maximum of the heights in *hd_list*
- is used.
- align : {'baseline', 'left', 'top', 'right', 'bottom', 'center'}
- Align mode.
- """
- if height is None:
- height = max(h for h, d in hd_list)
- cbook._check_in_list(
- ["baseline", "left", "top", "right", "bottom", "center"], align=align)
- if align == "baseline":
- height_descent = max(h - d for h, d in hd_list)
- descent = max(d for h, d in hd_list)
- height = height_descent + descent
- offsets = [0. for h, d in hd_list]
- elif align in ["left", "top"]:
- descent = 0.
- offsets = [d for h, d in hd_list]
- elif align in ["right", "bottom"]:
- descent = 0.
- offsets = [height - h + d for h, d in hd_list]
- elif align == "center":
- descent = 0.
- offsets = [(height - h) * .5 + d for h, d in hd_list]
- return height, descent, offsets
- class OffsetBox(martist.Artist):
- """
- The OffsetBox is 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, **kwargs)
- # Clipping has not been implemented in the OffesetBox 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`
- """
- martist.Artist.set_figure(self, 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
- """
- inside, info = self._default_contains(mouseevent)
- if inside is not None:
- return inside, info
- 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
- def get_offset(self, width, height, xdescent, ydescent, 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
- ----------
- width, height, xdescent, ydescent
- Extent parameters.
- renderer : `.RendererBase` subclass
- """
- return (self._offset(width, height, xdescent, ydescent, 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_extent_offsets(self, renderer):
- """
- Update offset of the children and return the extent of the box.
- Parameters
- ----------
- renderer : `.RendererBase` subclass
- Returns
- -------
- width
- height
- xdescent
- ydescent
- list of (xoffset, yoffset) pairs
- """
- raise NotImplementedError(
- "get_extent_offsets must be overridden in derived classes.")
- def get_extent(self, renderer):
- """Return a tuple ``width, height, xdescent, ydescent`` of the box."""
- w, h, xd, yd, offsets = self.get_extent_offsets(renderer)
- return w, h, xd, yd
- def get_window_extent(self, renderer):
- """Return the bounding box (`.Bbox`) in display space."""
- w, h, xd, yd, offsets = self.get_extent_offsets(renderer)
- px, py = self.get_offset(w, h, xd, yd, renderer)
- return mtransforms.Bbox.from_bounds(px - xd, py - yd, w, h)
- def draw(self, renderer):
- """
- Update the location of children if necessary and draw them
- to the given *renderer*.
- """
- width, height, xdescent, ydescent, offsets = self.get_extent_offsets(
- renderer)
- px, py = self.get_offset(width, height, xdescent, ydescent, 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=None, sep=None, width=None, height=None,
- align=None, mode=None,
- children=None):
- """
- Parameters
- ----------
- pad : float, optional
- The boundary padding in points.
- sep : float, optional
- 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'}
- Alignment of boxes.
- mode : {'fixed', 'expand', 'equal'}
- The packing mode.
- - 'fixed' packs the given `.Artists` 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 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):
- """
- The VPacker has its children packed vertically. It automatically
- adjusts the relative positions of children at drawing time.
- """
- def __init__(self, pad=None, sep=None, width=None, height=None,
- align="baseline", mode="fixed",
- children=None):
- """
- Parameters
- ----------
- pad : float, optional
- The boundary padding in points.
- sep : float, optional
- 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'}
- Alignment of boxes.
- mode : {'fixed', 'expand', 'equal'}
- The packing mode.
- - 'fixed' packs the given `.Artists` 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 in pixels.
- """
- super().__init__(pad, sep, width, height, align, mode, children)
- def get_extent_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)
- whd_list = [c.get_extent(renderer)
- for c in self.get_visible_children()]
- whd_list = [(w, h, xd, (h - yd)) for w, h, xd, yd in whd_list]
- wd_list = [(w, xd) for w, h, xd, yd in whd_list]
- width, xdescent, xoffsets = _get_aligned_offsets(wd_list,
- self.width,
- self.align)
- pack_list = [(h, yd) for w, h, xd, yd in whd_list]
- height, yoffsets_ = _get_packed_offsets(pack_list, self.height,
- sep, self.mode)
- yoffsets = yoffsets_ + [yd for w, h, xd, yd in whd_list]
- ydescent = height - yoffsets[0]
- yoffsets = height - yoffsets
- yoffsets = yoffsets - ydescent
- return (width + 2 * pad, height + 2 * pad,
- xdescent + pad, ydescent + pad,
- list(zip(xoffsets, yoffsets)))
- class HPacker(PackerBase):
- """
- The HPacker has its children packed horizontally. It automatically
- adjusts the relative positions of children at draw time.
- """
- def __init__(self, pad=None, sep=None, width=None, height=None,
- align="baseline", mode="fixed",
- children=None):
- """
- Parameters
- ----------
- pad : float, optional
- The boundary padding in points.
- sep : float, optional
- 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'}
- Alignment of boxes.
- mode : {'fixed', 'expand', 'equal'}
- The packing mode.
- - 'fixed' packs the given `.Artists` 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 in pixels.
- """
- super().__init__(pad, sep, width, height, align, mode, children)
- def get_extent_offsets(self, renderer):
- # docstring inherited
- dpicor = renderer.points_to_pixels(1.)
- pad = self.pad * dpicor
- sep = self.sep * dpicor
- whd_list = [c.get_extent(renderer)
- for c in self.get_visible_children()]
- if not whd_list:
- return 2 * pad, 2 * pad, pad, pad, []
- if self.height is None:
- height_descent = max(h - yd for w, h, xd, yd in whd_list)
- ydescent = max(yd for w, h, xd, yd in whd_list)
- height = height_descent + ydescent
- else:
- height = self.height - 2 * pad # width w/o pad
- hd_list = [(h, yd) for w, h, xd, yd in whd_list]
- height, ydescent, yoffsets = _get_aligned_offsets(hd_list,
- self.height,
- self.align)
- pack_list = [(w, xd) for w, h, xd, yd in whd_list]
- width, xoffsets_ = _get_packed_offsets(pack_list, self.width,
- sep, self.mode)
- xoffsets = xoffsets_ + [xd for w, h, xd, yd in whd_list]
- xdescent = whd_list[0][2]
- xoffsets = xoffsets - xdescent
- return (width + 2 * pad, height + 2 * pad,
- xdescent + pad, ydescent + pad,
- list(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.
- """
- def __init__(self, child, pad=None, draw_frame=False, patch_attrs=None):
- """
- Parameters
- ----------
- child : `~matplotlib.artist.Artist`
- The contained `.Artist`.
- pad : float
- 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_extent_offsets(self, renderer):
- # docstring inherited.
- dpicor = renderer.points_to_pixels(1.)
- pad = self.pad * dpicor
- w, h, xd, yd = self._children[0].get_extent(renderer)
- return (w + 2 * pad, h + 2 * pad, xd + pad, yd + pad,
- [(0, 0)])
- def draw(self, renderer):
- # docstring inherited
- width, height, xdescent, ydescent, offsets = self.get_extent_offsets(
- renderer)
- px, py = self.get_offset(width, height, xdescent, ydescent, 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)
- #bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
- self.stale = False
- def update_frame(self, bbox, fontsize=None):
- self.patch.set_bounds(bbox.x0, bbox.y0, bbox.width, bbox.height)
- 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_window_extent(self, renderer):
- """Return the bounding box in display space."""
- w, h, xd, yd = self.get_extent(renderer)
- ox, oy = self.get_offset() # w, h, xd, yd)
- return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h)
- def get_extent(self, renderer):
- """Return width, height, xdescent, ydescent of box."""
- dpi_cor = renderer.points_to_pixels(1.)
- return (self.width * dpi_cor, self.height * dpi_cor,
- self.xdescent * dpi_cor, self.ydescent * 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.figure
- 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 contains a single Text instance. The text is
- placed at (0, 0) with baseline+left alignment. The width and height
- of the TextArea instance is the width and height of the its child
- text.
- """
- def __init__(self, s,
- textprops=None,
- multilinebaseline=None,
- minimumdescent=True,
- ):
- """
- Parameters
- ----------
- s : str
- The text to be displayed.
- textprops : dict, optional
- Dictionary of keyword parameters to be passed to the
- `~matplotlib.text.Text` instance contained inside TextArea.
- multilinebaseline : bool, optional
- If `True`, baseline for multiline text is adjusted so that it is
- (approximately) center-aligned with singleline text.
- minimumdescent : bool, optional
- If `True`, the box has a minimum descent of "p".
- """
- if textprops is None:
- textprops = {}
- textprops.setdefault("va", "baseline")
- self._text = mtext.Text(0, 0, s, **textprops)
- OffsetBox.__init__(self)
- 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
- self._minimumdescent = minimumdescent
- 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, baseline for multiline text is adjusted so that it is
- (approximately) center-aligned with single-line text.
- """
- self._multilinebaseline = t
- self.stale = True
- def get_multilinebaseline(self):
- """
- Get multilinebaseline.
- """
- return self._multilinebaseline
- def set_minimumdescent(self, t):
- """
- Set minimumdescent.
- If True, extent of the single line text is adjusted so that
- it has minimum descent of "p"
- """
- self._minimumdescent = t
- self.stale = True
- def get_minimumdescent(self):
- """
- Get minimumdescent.
- """
- return self._minimumdescent
- 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_window_extent(self, renderer):
- """Return the bounding box in display space."""
- w, h, xd, yd = self.get_extent(renderer)
- ox, oy = self.get_offset() # w, h, xd, yd)
- return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h)
- def get_extent(self, renderer):
- _, h_, d_ = renderer.get_text_width_height_descent(
- "lp", self._text._fontproperties, ismath=False)
- bbox, info, d = self._text._get_layout(renderer)
- w, h = bbox.width, bbox.height
- self._baseline_transform.clear()
- if len(info) > 1 and self._multilinebaseline:
- d_new = 0.5 * h - 0.5 * (h_ - d_)
- self._baseline_transform.translate(0, d - d_new)
- d = d_new
- else: # single line
- h_d = max(h_ - d_, h - d)
- if self.get_minimumdescent():
- ## to have a minimum descent, #i.e., "l" and "p" have same
- ## descents.
- d = max(d, d_)
- #else:
- # d = d
- h = h_d + d
- return w, h, 0., d
- 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):
- """
- Offset Box with the aux_transform. Its children will be
- transformed with the aux_transform first then will be
- offsetted. The absolute coordinate of the aux_transform is meaning
- as it will be automatically adjust so that the left-lower corner
- of the bounding box of children will be set to (0, 0) before the
- offset transform.
- It is similar to drawing area, except that the extent of the box
- is not predetermined but calculated from the window extent of its
- children. Furthermore, the extent of the children will be
- calculated in the transformed coordinate.
- """
- def __init__(self, aux_transform):
- self.aux_transform = aux_transform
- OffsetBox.__init__(self)
- 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 :class:`~matplotlib.transforms.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_window_extent(self, renderer):
- """Return the bounding box in display space."""
- w, h, xd, yd = self.get_extent(renderer)
- ox, oy = self.get_offset() # w, h, xd, yd)
- return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h)
- def get_extent(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 = mtransforms.Bbox.union(bboxes)
- # adjust ref_offset_transform
- self.ref_offset_transform.translate(-ub.x0, -ub.y0)
- # restor offset transform
- self.offset_transform.set_matrix(_off)
- return ub.width, ub.height, 0., 0.
- 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 offset box 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. Supported values:
- - 'upper right'
- - 'upper left'
- - 'lower left'
- - 'lower right'
- - 'center left'
- - 'center right'
- - 'lower center'
- - 'upper center'
- - 'center'
- 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 = cbook._check_getitem(self.codes, loc=loc)
- self.loc = loc
- self.borderpad = borderpad
- self.pad = pad
- if prop is None:
- self.prop = FontProperties(size=rcParams["legend.fontsize"])
- else:
- self.prop = FontProperties._from_any(prop)
- if isinstance(prop, dict) and "size" not in prop:
- self.prop.set_size(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_extent(self, renderer):
- """
- Return the extent of the box as (width, height, x, y).
- This is the extent of the child plus the padding.
- """
- w, h, xd, yd = self.get_child().get_extent(renderer)
- fontsize = renderer.points_to_pixels(self.prop.get_size_in_points())
- pad = self.pad * fontsize
- return w + 2 * pad, h + 2 * pad, xd + pad, yd + 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("Invalid argument for bbox : %s" %
- str(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
- def get_window_extent(self, renderer):
- """Return the bounding box in display space."""
- self._update_offset_func(renderer)
- w, h, xd, yd = self.get_extent(renderer)
- ox, oy = self.get_offset(w, h, xd, yd, renderer)
- return Bbox.from_bounds(ox - xd, oy - yd, w, h)
- def _update_offset_func(self, renderer, fontsize=None):
- """
- Update the offset func which depends on the dpi of the
- renderer (because of the padding).
- """
- if fontsize is None:
- fontsize = renderer.points_to_pixels(
- self.prop.get_size_in_points())
- def _offset(w, h, xd, yd, renderer, fontsize=fontsize, self=self):
- bbox = Bbox.from_bounds(0, 0, w, h)
- borderpad = self.borderpad * fontsize
- bbox_to_anchor = self.get_bbox_to_anchor()
- x0, y0 = self._get_anchored_bbox(self.loc,
- bbox,
- bbox_to_anchor,
- borderpad)
- return x0 + xd, y0 + yd
- self.set_offset(_offset)
- def update_frame(self, bbox, fontsize=None):
- self.patch.set_bounds(bbox.x0, bbox.y0, bbox.width, bbox.height)
- if fontsize:
- self.patch.set_mutation_scale(fontsize)
- def draw(self, renderer):
- # docstring inherited
- if not self.get_visible():
- return
- fontsize = renderer.points_to_pixels(self.prop.get_size_in_points())
- self._update_offset_func(renderer, fontsize)
- # update the location and size of the legend
- bbox = self.get_window_extent(renderer)
- self.update_frame(bbox, fontsize)
- self.patch.draw(renderer)
- width, height, xdescent, ydescent = self.get_extent(renderer)
- px, py = self.get_offset(width, height, xdescent, ydescent, renderer)
- self.get_child().set_offset((px, py))
- self.get_child().draw(renderer)
- self.stale = False
- def _get_anchored_bbox(self, loc, bbox, parentbbox, borderpad):
- """
- Return the position of the bbox anchored at the parentbbox
- with the loc code, with the borderpad.
- """
- assert loc in range(1, 11) # called only internally
- BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11)
- anchor_coefs = {UR: "NE",
- UL: "NW",
- LL: "SW",
- LR: "SE",
- R: "E",
- CL: "W",
- CR: "E",
- LC: "S",
- UC: "N",
- C: "C"}
- c = anchor_coefs[loc]
- container = parentbbox.padded(-borderpad)
- anchored_box = bbox.anchored(c, container=container)
- return anchored_box.x0, anchored_box.y0
- 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 = {'ha', 'horizontalalignment', 'va', 'verticalalignment'}
- if badkwargs & set(prop):
- raise ValueError(
- "Mixing horizontalalignment or verticalalignment with "
- "AnchoredText is not supported.")
- self.txt = TextArea(s, textprops=prop, minimumdescent=False)
- 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
- ):
- OffsetBox.__init__(self)
- 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_window_extent(self, renderer):
- """Return the bounding box in display space."""
- w, h, xd, yd = self.get_extent(renderer)
- ox, oy = self.get_offset()
- return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h)
- def get_extent(self, renderer):
- if self._dpi_cor: # True, do correction
- dpi_cor = renderer.points_to_pixels(1.)
- else:
- dpi_cor = 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 w, h, 0, 0
- 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 "AnnotationBbox(%g,%g)" % (self.xy[0], self.xy[1])
- @docstring.dedent_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 : str or `.Artist` or `.Transform` or callable or \
- (float, float), default: 'data'
- The coordinate system that *xy* is given in. See the parameter
- *xycoords* in `.Annotation` for a detailed description.
- boxcoords : str or `.Artist` or `.Transform` or callable or \
- (float, float), 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
- Whether to draw a frame around the box.
- 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).
- **kwargs
- Other parameters are identical to `.Annotation`.
- """
- martist.Artist.__init__(self, **kwargs)
- mtext._AnnotationBase.__init__(self,
- xy,
- xycoords=xycoords,
- annotation_clip=annotation_clip)
- self.offsetbox = offsetbox
- self.arrowprops = arrowprops
- self.set_fontsize(fontsize)
- if xybox is None:
- self.xybox = xy
- else:
- self.xybox = xybox
- if boxcoords is None:
- self.boxcoords = xycoords
- else:
- self.boxcoords = boxcoords
- 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._box_alignment = box_alignment
- # frame
- 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,
- )
- self.patch.set_boxstyle("square", pad=pad)
- if bboxprops:
- self.patch.set(**bboxprops)
- @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):
- inside, info = self._default_contains(mouseevent)
- if inside is not None:
- return inside, info
- if not self._check_xy(None):
- return False, {}
- return self.offsetbox.contains(mouseevent)
- #if self.arrow_patch is not None:
- # a, ainfo=self.arrow_patch.contains(event)
- # t = t or a
- # 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 = rcParams["legend.fontsize"]
- self.prop = FontProperties(size=s)
- self.stale = True
- @cbook._delete_parameter("3.3", "s")
- def get_fontsize(self, s=None):
- """Return the fontsize in points."""
- return self.prop.get_size_in_points()
- def get_window_extent(self, renderer):
- """
- get the bounding box in display space.
- """
- bboxes = [child.get_window_extent(renderer)
- for child in self.get_children()]
- return Bbox.union(bboxes)
- def get_tightbbox(self, renderer):
- """
- get tight bounding box in display space.
- """
- bboxes = [child.get_tightbbox(renderer)
- for child in self.get_children()]
- return Bbox.union(bboxes)
- def update_positions(self, renderer):
- """
- Update the pixel positions of the annotated point and the text.
- """
- xy_pixel = self._get_position_xy(renderer)
- self._update_position_xybox(renderer, xy_pixel)
- mutation_scale = renderer.points_to_pixels(self.get_fontsize())
- self.patch.set_mutation_scale(mutation_scale)
- if self.arrow_patch:
- self.arrow_patch.set_mutation_scale(mutation_scale)
- def _update_position_xybox(self, renderer, xy_pixel):
- """
- Update the pixel positions of the annotation text and the arrow patch.
- """
- x, y = self.xybox
- if isinstance(self.boxcoords, tuple):
- xcoord, ycoord = self.boxcoords
- x1, y1 = self._get_xy(renderer, x, y, xcoord)
- x2, y2 = self._get_xy(renderer, x, y, ycoord)
- ox0, oy0 = x1, y2
- else:
- ox0, oy0 = self._get_xy(renderer, x, y, self.boxcoords)
- w, h, xd, yd = self.offsetbox.get_extent(renderer)
- _fw, _fh = self._box_alignment
- self.offsetbox.set_offset((ox0 - _fw * w + xd, oy0 - _fh * h + yd))
- # update patch position
- bbox = self.offsetbox.get_window_extent(renderer)
- #self.offsetbox.set_offset((ox0-_fw*w, oy0-_fh*h))
- self.patch.set_bounds(bbox.x0, bbox.y0,
- bbox.width, bbox.height)
- x, y = xy_pixel
- ox1, oy1 = x, y
- if self.arrowprops:
- d = self.arrowprops.copy()
- # 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.
- relpos = self._arrow_relpos
- ox0 = bbox.x0 + bbox.width * relpos[0]
- oy0 = bbox.y0 + bbox.height * relpos[1]
- # The arrow will be drawn from (ox0, oy0) to (ox1,
- # oy1). It will be first clipped by patchA and patchB.
- # Then it will be shrunk by shrinkA and shrinkB
- # (in points). If patch A is not set, self.bbox_patch
- # is used.
- self.arrow_patch.set_positions((ox0, oy0), (ox1, oy1))
- fs = self.prop.get_size_in_points()
- mutation_scale = d.pop("mutation_scale", fs)
- mutation_scale = renderer.points_to_pixels(mutation_scale)
- self.arrow_patch.set_mutation_scale(mutation_scale)
- patchA = d.pop("patchA", self.patch)
- self.arrow_patch.set_patchA(patchA)
- def draw(self, renderer):
- # docstring inherited
- if renderer is not None:
- self._renderer = renderer
- if not self.get_visible() or not self._check_xy(renderer):
- return
- self.update_positions(renderer)
- if self.arrow_patch is not None:
- if self.arrow_patch.figure is None and self.figure is not None:
- self.arrow_patch.figure = self.figure
- self.arrow_patch.draw(renderer)
- self.patch.draw(renderer)
- self.offsetbox.draw(renderer)
- 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
- self.got_artist = False
- self.canvas = self.ref_artist.figure.canvas
- self._use_blit = use_blit and self.canvas.supports_blit
- c2 = self.canvas.mpl_connect('pick_event', self.on_pick)
- c3 = self.canvas.mpl_connect('button_release_event', self.on_release)
- if not ref_artist.pickable():
- ref_artist.set_picker(True)
- overridden_picker = cbook._deprecate_method_override(
- __class__.artist_picker, self, since="3.3",
- addendum="Directly set the artist's picker if desired.")
- if overridden_picker is not None:
- ref_artist.set_picker(overridden_picker)
- self.cids = [c2, c3]
- 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.figure._cachedRenderer)
- self.canvas.blit()
- else:
- self.canvas.draw()
- @cbook.deprecated("3.3", alternative="self.on_motion")
- def on_motion_blit(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)
- self.canvas.restore_region(self.background)
- self.ref_artist.draw(self.ref_artist.figure._cachedRenderer)
- self.canvas.blit()
- def on_pick(self, evt):
- if self._check_still_parented() and evt.artist == self.ref_artist:
- self.mouse_x = evt.mouseevent.x
- self.mouse_y = evt.mouseevent.y
- self.got_artist = True
- if self._use_blit:
- self.ref_artist.set_animated(True)
- self.canvas.draw()
- self.background = \
- self.canvas.copy_from_bbox(self.ref_artist.figure.bbox)
- self.ref_artist.draw(self.ref_artist.figure._cachedRenderer)
- self.canvas.blit()
- self._c1 = self.canvas.mpl_connect(
- "motion_notify_event", self.on_motion)
- self.save_offset()
- def on_release(self, event):
- if self._check_still_parented() and self.got_artist:
- self.finalize_offset()
- self.got_artist = False
- self.canvas.mpl_disconnect(self._c1)
- if self._use_blit:
- self.ref_artist.set_animated(False)
- def _check_still_parented(self):
- if self.ref_artist.figure is None:
- self.disconnect()
- return False
- else:
- return True
- def disconnect(self):
- """Disconnect the callbacks."""
- for cid in self.cids:
- self.canvas.mpl_disconnect(cid)
- try:
- c1 = self._c1
- except AttributeError:
- pass
- else:
- self.canvas.mpl_disconnect(c1)
- @cbook.deprecated("3.3", alternative="self.ref_artist.contains")
- def artist_picker(self, artist, evt):
- return self.ref_artist.contains(evt)
- 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):
- DraggableBase.__init__(self, ref_artist, use_blit=use_blit)
- self.offsetbox = offsetbox
- def save_offset(self):
- offsetbox = self.offsetbox
- renderer = offsetbox.figure._cachedRenderer
- w, h, xd, yd = offsetbox.get_extent(renderer)
- offset = offsetbox.get_offset(w, h, xd, yd, 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.figure._cachedRenderer
- w, h, xd, yd = offsetbox.get_extent(renderer)
- ox, oy = offsetbox._offset
- loc_in_canvas = (ox - xd, oy - yd)
- return loc_in_canvas
- class DraggableAnnotation(DraggableBase):
- def __init__(self, annotation, use_blit=False):
- DraggableBase.__init__(self, 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))
|