""" The OffsetBox is a simple container artist. The child artist are meant to be drawn at a relative position to its parent. The [VH]Packer, DrawingArea and TextArea are derived from the OffsetBox.
The [VH]Packer automatically adjust the relative postisions of their children, which should be instances of the OffsetBox. This is used to align similar artists together, e.g., in legend.
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 TextArea is contains a single Text instance. The width and height of the TextArea instance is the width and height of the its child text. """
# for debugging use 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.
""" Geiven 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.
*wd_list* : list of (width, xdescent) of boxes to be packed. *sep* : spacing between boxes *total* : Intended total length. None if not used. *mode* : packing mode. 'fixed', 'expand', or 'equal'. """
# d_list is currently not used.
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: total = (maxh + sep) * len(w_list) else: sep = total / len(w_list) - maxh offsets = (maxh + sep) * np.arange(len(w_list)) return total, offsets
else: raise ValueError("Unknown mode : %s" % (mode,))
""" 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.
*hd_list* : list of (width, xdescent) of boxes to be aligned. *sep* : spacing between boxes *height* : Intended total length. None if not used. *align* : align mode. 'baseline', 'top', 'bottom', or 'center'. """
descent = 0. offsets = [d for h, d in hd_list] descent = 0. offsets = [height - h + d for h, d in hd_list] else: raise ValueError("Unknown Align mode : %s" % (align,))
""" The OffsetBox is a simple container artist. The child artist are meant to be drawn at a relative position to its parent. """
# 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.
""" Set the figure
accepts a class:`~matplotlib.figure.Figure` instance """
def axes(self, ax): # TODO deal with this better if c is not None: c.axes = ax
for c in self.get_children(): a, b = c.contains(mouseevent) if a: return a, b return False, {}
""" Set the offset.
Parameters ---------- xy : (float, float) or callable The (x,y) coordinates of the offset in display units. A callable must have the signature::
def offset(width, height, xdescent, ydescent, renderer) \ -> (float, float) """
""" Get the offset
accepts extent of the box """ if callable(self._offset) else self._offset)
""" Set the width
accepts float """ self.width = width self.stale = True
""" Set the height
accepts float """ self.height = height self.stale = True
""" Return a list of visible artists it contains. """
""" Return a list of artists it contains. """
raise Exception("")
""" Return with, height, xdescent, ydescent of box """
''' get the bounding box in display space. '''
""" Update the location of children if necessary and draw them to the given *renderer*. """
renderer)
align=None, mode=None, children=None): """ Parameters ---------- pad : float, optional Boundary pad.
sep : float, optional Spacing between items.
width : float, optional
height : float, optional Width and height of the container box, calculated if `None`.
align : str, optional Alignment of boxes. Can be one of ``top``, ``bottom``, ``left``, ``right``, ``center`` and ``baseline``
mode : str, optional Packing mode.
Notes ----- *pad* and *sep* need to given in points and will be scale with the renderer dpi, while *width* and *height* need to be in pixels. """
""" The VPacker has its children packed vertically. It automatically adjust the relative positions of children in the drawing time. """ align="baseline", mode="fixed", children=None): """ Parameters ---------- pad : float, optional Boundary pad.
sep : float, optional Spacing between items.
width : float, optional
height : float, optional
width and height of the container box, calculated if `None`.
align : str, optional Alignment of boxes.
mode : str, optional Packing mode.
Notes ----- *pad* and *sep* need to given in points and will be scale with the renderer dpi, while *width* and *height* need to be in pixels. """
""" update offset of childrens and return the extents of the box """
for c in self.get_visible_children(): if isinstance(c, PackerBase) and c.mode == "expand": c.set_width(self.width)
for c in self.get_visible_children()]
self.width, self.align)
sep, self.mode)
xdescent + pad, ydescent + pad, list(zip(xoffsets, yoffsets)))
""" The HPacker has its children packed horizontally. It automatically adjusts the relative positions of children at draw time. """ align="baseline", mode="fixed", children=None): """ Parameters ---------- pad : float, optional Boundary pad.
sep : float, optional Spacing between items.
width : float, optional
height : float, optional Width and height of the container box, calculated if `None`.
align : str Alignment of boxes.
mode : str Packing mode.
Notes ----- *pad* and *sep* need to given in points and will be scale with the renderer dpi, while *width* and *height* need to be in pixels. """
""" update offset of children and return the extents of the box """
for c in self.get_visible_children()]
return 2 * pad, 2 * pad, pad, pad, []
else: height = self.height - 2 * pad # width w/o pad
self.height, self.align)
sep, self.mode)
xdescent + pad, ydescent + pad, list(zip(xoffsets, yoffsets)))
""" *pad* : boundary pad
.. note:: *pad* need to given in points and will be scale with the renderer dpi, while *width* and *height* need to be in pixels. """
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 )
self.patch.set_boxstyle("square", pad=0)
if patch_attrs is not None: self.patch.update(patch_attrs)
self._drawFrame = draw_frame
""" update offset of childrens and return the extents of the box """
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)]
""" 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))
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
self.patch.set_bounds(bbox.x0, bbox.y0, bbox.width, bbox.height)
if fontsize: self.patch.set_mutation_scale(fontsize) self.stale = True
# update the location and size of the legend bbox = self.get_window_extent(renderer) self.update_frame(bbox)
if self._drawFrame: self.patch.draw(renderer)
""" 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. """
ydescent=0., clip=False): """ *width*, *height* : width and height of the container box. *xdescent*, *ydescent* : descent of the box in x- and y-direction. *clip* : Whether to clip the children """
def clip_children(self): """ If the children of this DrawingArea should be clipped by DrawingArea bounding box. """ return self._clip_children
def clip_children(self, val): self._clip_children = bool(val) self.stale = True
""" Return the :class:`~matplotlib.transforms.Transform` applied to the children """
""" set_transform is ignored. """ pass
""" Set the offset of the container.
Parameters ---------- xy : (float, float) The (x,y) coordinates of the offset in display units. """
""" return offset of the container. """ return self._offset
''' get 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)
""" Return with, height, xdescent, ydescent of box """
self.xdescent * dpi_cor, self.ydescent * dpi_cor
'Add any :class:`~matplotlib.artist.Artist` to the container box' a.set_transform(self.get_transform()) a.axes = self.axes a.set_figure(fig)
""" Draw the children """
# At this point the DrawingArea has a transform # to the display space so the path created is # good for clipping children mpath.Path([[0, 0], [0, self.height], [self.width, self.height], [self.width, 0]]), self.get_transform()) c.set_clip_path(tpath)
""" 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. """ textprops=None, multilinebaseline=None, minimumdescent=True, ): """ Parameters ---------- s : str a string to be displayed.
textprops : `~matplotlib.font_manager.FontProperties`, optional
multilinebaseline : bool, optional If `True`, baseline for multiline text is adjusted so that it is (approximatedly) center-aligned with singleline text.
minimumdescent : bool, optional If `True`, the box has a minimum descent of "p". """
self._baseline_transform)
"Set the text of this area as a string." self._text.set_text(s) self.stale = True
"Returns the string representation of this area's text" return self._text.get_text()
""" Set multilinebaseline .
If True, baseline for multiline text is adjusted so that it is (approximatedly) center-aligned with singleline text. """ self._multilinebaseline = t self.stale = True
""" get multilinebaseline . """ return self._multilinebaseline
""" Set minimumdescent .
If True, extent of the single line text is adjusted so that it has minimum descent of "p" """
""" get minimumdescent. """
""" set_transform is ignored. """ pass
""" Set the offset of the container.
Parameters ---------- xy : (float, float) The (x,y) coordinates of the offset in display units. """
""" return offset of the container. """ return self._offset
''' get 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)
"lp", self._text._fontproperties, ismath=False)
d_new = 0.5 * h - 0.5 * (h_ - d_) self._baseline_transform.translate(0, d - d_new) d = d_new
else: # single line
## to have a minimum descent, #i.e., "l" and "p" have same ## descents. #else: # d = d
""" Draw the children """
""" Offset Box with the aux_transform. Its children will be transformed with the aux_transform first then will be offseted. 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. """ self.aux_transform = aux_transform OffsetBox.__init__(self)
self.offset_transform = mtransforms.Affine2D() self.offset_transform.clear() self.offset_transform.translate(0, 0)
# ref_offset_transform is used to make the offset_transform is # always reference to the lower-left corner of the bbox of its # children. self.ref_offset_transform = mtransforms.Affine2D() self.ref_offset_transform.clear()
'Add any :class:`~matplotlib.artist.Artist` to the container box' self._children.append(a) a.set_transform(self.get_transform()) self.stale = True
""" Return the :class:`~matplotlib.transforms.Transform` applied to the children """ return self.aux_transform + \ self.ref_offset_transform + \ self.offset_transform
""" set_transform is ignored. """ pass
""" 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
""" return offset of the container. """ return self._offset
''' get 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)
# clear the offset transforms _off = self.offset_transform.to_values() # 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_tansform self.ref_offset_transform.translate(-ub.x0, -ub.y0)
# restor offset transform mtx = self.offset_transform.matrix_from_values(*_off) self.offset_transform.set_matrix(mtx)
return ub.width, ub.height, 0., 0.
""" Draw the children """
for c in self._children: c.draw(renderer)
bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) self.stale = False
""" An offset box placed according to the legend location loc. AnchoredOffsetbox has a single child. When multiple children is needed, use other OffsetBox class to enclose them. By default, the offset box is anchored against its parent axes. You may explicitly specify the bbox_to_anchor. """
# Location codes '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, }
pad=0.4, borderpad=0.5, child=None, prop=None, frameon=True, bbox_to_anchor=None, bbox_transform=None, **kwargs): """ loc is a string or an integer specifying the legend location. The valid location codes are::
'upper right' : 1, 'upper left' : 2, 'lower left' : 3, 'lower right' : 4, 'right' : 5, (same as 'center right', for back-compatibility) 'center left' : 6, 'center right' : 7, 'lower center' : 8, 'upper center' : 9, 'center' : 10,
pad : pad around the child for drawing a frame. given in fraction of fontsize.
borderpad : pad between offsetbox frame and the bbox_to_anchor,
child : OffsetBox instance that will be anchored.
prop : font property. This is only used as a reference for paddings.
frameon : draw a frame box if True.
bbox_to_anchor : bbox to anchor. Use self.axes.bbox if None.
bbox_transform : with which the bbox_to_anchor will be transformed.
""" super().__init__(**kwargs)
self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform) self.set_child(child)
if isinstance(loc, str): try: loc = self.codes[loc] except KeyError: raise ValueError('Unrecognized location "%s". Valid ' 'locations are\n\t%s\n' % (loc, '\n\t'.join(self.codes)))
self.loc = loc self.borderpad = borderpad self.pad = pad
if prop is None: self.prop = FontProperties(size=rcParams["legend.fontsize"]) elif isinstance(prop, dict): self.prop = FontProperties(**prop) if "size" not in prop: self.prop.set_size(rcParams["legend.fontsize"]) else: self.prop = prop
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 ) self.patch.set_boxstyle("square", pad=0) self._drawFrame = frameon
"set the child to be anchored" self._child = child if child is not None: child.axes = self.axes self.stale = True
"return the child" return self._child
"return the list of children" return [self._child]
""" return the extent of the artist. The extent of the child added with the pad is returned """ 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
""" return the bbox that the legend will be anchored """ 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)
""" set the bbox that the child will be anchored.
*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: raise ValueError("Invalid argument for bbox : %s" % str(bbox))
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
''' get 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)
""" 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)
self.patch.set_bounds(bbox.x0, bbox.y0, bbox.width, bbox.height)
if fontsize: self.patch.set_mutation_scale(fontsize)
"draw the artist"
if not self.get_visible(): return
fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) self._update_offset_func(renderer, fontsize)
if self._drawFrame: # 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
""" 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
""" AnchoredOffsetbox with Text. """
""" Parameters ---------- s : string Text.
loc : str Location code.
pad : float, optional Pad between the text and the frame as fraction of the font size.
borderpad : float, optional Pad between the frame and the axes (or *bbox_to_anchor*).
prop : dictionary, optional, default: None Dictionary of keyword parameters to be passed to the `~matplotlib.text.Text` instance contained inside AnchoredText.
Notes ----- Other keyword parameters of `AnchoredOffsetbox` are also allowed. """
if prop is None: prop = {} badkwargs = {'ha', 'horizontalalignment', 'va', 'verticalalignment'} if badkwargs & set(prop): warnings.warn("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)
zoom=1, cmap=None, norm=None, interpolation=None, origin=None, filternorm=1, 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)
self._data = np.asarray(arr) self.image.set_data(self._data) self.stale = True
return self._data
self._zoom = zoom self.stale = True
return self._zoom
# def set_axes(self, axes): # self.image.set_axes(axes) # martist.Artist.set_axes(self, axes)
# 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])
""" return offset of the container. """ return self._offset
return [self.image]
''' get 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)
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
""" Draw the children """ self.image.draw(renderer) # bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) self.stale = False
""" Annotation-like class, but with offsetbox instead of Text. """
def __str__(self): return "AnnotationBbox(%g,%g)" % (self.xy[0], self.xy[1])
xybox=None, xycoords='data', boxcoords=None, frameon=True, pad=0.4, # BboxPatch annotation_clip=None, box_alignment=(0.5, 0.5), bboxprops=None, arrowprops=None, fontsize=None, **kwargs): """ *offsetbox* : OffsetBox instance
*xycoords* : same as Annotation but can be a tuple of two strings which are interpreted as x and y coordinates.
*boxcoords* : similar to textcoords as Annotation but can be a tuple of two strings which are interpreted as x and y coordinates.
*box_alignment* : 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).
other parameters are identical to that of Annotation. """
martist.Artist.__init__(self, **kwargs) _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._fw, self._fh = 0., 0. # for alignment 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 ) self.patch.set_boxstyle("square", pad=pad) if bboxprops: self.patch.set(**bboxprops) self._drawFrame = frameon
def xyann(self): return self.xybox
def xyann(self, xyann): self.xybox = xyann self.stale = True
def anncoords(self): return self.boxcoords
def anncoords(self, coords): self.boxcoords = coords self.stale = True
t, tinfo = self.offsetbox.contains(event) #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
return t, tinfo
children = [self.offsetbox, self.patch] if self.arrow_patch: children.append(self.arrow_patch) return children
if self.arrow_patch is not None: self.arrow_patch.set_figure(fig) self.offsetbox.set_figure(fig) martist.Artist.set_figure(self, fig)
""" set fontsize in points """ if s is None: s = rcParams["legend.fontsize"]
self.prop = FontProperties(size=s) self.stale = True
""" return fontsize in points """ return self.prop.get_size_in_points()
""" 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)
""" 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: x0, y0 = x, y
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)
""" Draw the :class:`Annotation` object to the given *renderer*. """
if renderer is not None: self._renderer = renderer if not self.get_visible(): return
xy_pixel = self._get_position_xy(renderer)
if not self._check_xy(renderer, xy_pixel): 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)
if self._drawFrame: self.patch.draw(renderer)
self.offsetbox.draw(renderer) self.stale = False
""" helper code for a draggable artist (legend, offsetbox) The derived class must override following two method.
def save_offset(self): pass
def update_offset(self, dx, dy): pass
*save_offset* is called when the object is picked for dragging and it is meant to save reference position of the artist.
*update_offset* is called during the dragging. dx and dy is the pixel offset from the point where the mouse drag started.
Optionally you may override following two methods.
def artist_picker(self, artist, evt): return self.ref_artist.contains(evt)
def finalize_offset(self): pass
*artist_picker* is a picker method that will be used. *finalize_offset* is called when the mouse is released. In current implementation of DraggableLegend and DraggableAnnotation, *update_offset* places the artists simply in display coordinates. And *finalize_offset* recalculate their position in the normalized axes coordinate and set a relavant attribute. """
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)
ref_artist.set_picker(self.artist_picker) self.cids = [c2, c3]
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.draw()
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(self.ref_artist.figure.bbox)
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.ref_artist.figure.bbox) self._c1 = self.canvas.mpl_connect('motion_notify_event', self.on_motion_blit) else: self._c1 = self.canvas.mpl_connect('motion_notify_event', self.on_motion) self.save_offset()
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)
if self.ref_artist.figure is None: self.disconnect() return False else: return True
"""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)
return self.ref_artist.contains(evt)
pass
pass
pass
DraggableBase.__init__(self, ref_artist, use_blit=use_blit) self.offsetbox = offsetbox
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)
loc_in_canvas = self.offsetbox_x + dx, self.offsetbox_y + dy self.offsetbox.set_offset(loc_in_canvas)
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
DraggableBase.__init__(self, annotation, use_blit=use_blit) self.annotation = annotation
ann = self.annotation self.ox, self.oy = ann.get_transform().transform(ann.xyann)
ann = self.annotation ann.xyann = ann.get_transform().inverted().transform( (self.ox + dx, self.oy + dy))
if __name__ == "__main__": import matplotlib.pyplot as plt fig = plt.figure(1) fig.clf() ax = plt.subplot(121)
#txt = ax.text(0.5, 0.5, "Test", size=30, ha="center", color="w") kwargs = dict()
a = np.arange(256).reshape(16, 16) / 256. myimage = OffsetImage(a, zoom=2, norm=None, origin=None, **kwargs ) ax.add_artist(myimage)
myimage.set_offset((100, 100))
myimage2 = OffsetImage(a, zoom=2, norm=None, origin=None, **kwargs ) ann = AnnotationBbox(myimage2, (0.5, 0.5), xybox=(30, 30), xycoords='data', boxcoords="offset points", frameon=True, pad=0.4, # BboxPatch bboxprops=dict(boxstyle="round", fc="y"), fontsize=None, arrowprops=dict(arrowstyle="->"), )
ax.add_artist(ann)
plt.draw() plt.show() |