1826 lines
56 KiB
Python
1826 lines
56 KiB
Python
|
"""
|
||
|
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 positions 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.
|
||
|
"""
|
||
|
|
||
|
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.text import _AnnotationBase
|
||
|
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.
|
||
|
|
||
|
*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'.
|
||
|
"""
|
||
|
|
||
|
w_list, d_list = zip(*wd_list)
|
||
|
# d_list is currently not used.
|
||
|
|
||
|
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:
|
||
|
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,))
|
||
|
|
||
|
|
||
|
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.
|
||
|
|
||
|
*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'.
|
||
|
"""
|
||
|
|
||
|
if height is None:
|
||
|
height = max(h for h, d in hd_list)
|
||
|
|
||
|
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]
|
||
|
else:
|
||
|
raise ValueError("Unknown Align mode : %s" % (align,))
|
||
|
|
||
|
return height, descent, offsets
|
||
|
|
||
|
|
||
|
class OffsetBox(martist.Artist):
|
||
|
"""
|
||
|
The OffsetBox is a simple container artist. The child artist are meant
|
||
|
to be drawn at a relative position to its parent.
|
||
|
"""
|
||
|
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
|
||
|
"""
|
||
|
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* 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.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
|
||
|
adjust the relative positions of children in the 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* need to given in points and will be scale with
|
||
|
the renderer dpi, while *width* and *height* need to be 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* need to given in points and will be scale with
|
||
|
the renderer dpi, while *width* and *height* need to be 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 *hight* are in *pixel* 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
|
||
|
)
|
||
|
|
||
|
self.patch.set_boxstyle("square", pad=0)
|
||
|
|
||
|
if patch_attrs is not None:
|
||
|
self.patch.update(patch_attrs)
|
||
|
|
||
|
self._drawFrame = draw_frame
|
||
|
|
||
|
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):
|
||
|
"""
|
||
|
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
|
||
|
|
||
|
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
|
||
|
bbox = self.get_window_extent(renderer)
|
||
|
self.update_frame(bbox)
|
||
|
|
||
|
if self._drawFrame:
|
||
|
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):
|
||
|
"""
|
||
|
*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
|
||
|
"""
|
||
|
|
||
|
super().__init__()
|
||
|
|
||
|
self.width = width
|
||
|
self.height = height
|
||
|
self.xdescent = xdescent
|
||
|
self.ydescent = ydescent
|
||
|
self._clip_children = clip
|
||
|
|
||
|
self.offset_transform = mtransforms.Affine2D()
|
||
|
self.offset_transform.clear()
|
||
|
self.offset_transform.translate(0, 0)
|
||
|
|
||
|
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 :class:`~matplotlib.transforms.Transform` applied
|
||
|
to the children
|
||
|
"""
|
||
|
return self.dpi_transform + self.offset_transform
|
||
|
|
||
|
def set_transform(self, t):
|
||
|
"""
|
||
|
set_transform is ignored.
|
||
|
"""
|
||
|
pass
|
||
|
|
||
|
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):
|
||
|
'''
|
||
|
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)
|
||
|
|
||
|
def get_extent(self, renderer):
|
||
|
"""
|
||
|
Return with, 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 any :class:`~matplotlib.artist.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):
|
||
|
"""
|
||
|
Draw the children
|
||
|
"""
|
||
|
|
||
|
dpi_cor = renderer.points_to_pixels(1.)
|
||
|
self.dpi_transform.clear()
|
||
|
self.dpi_transform.scale(dpi_cor, 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
|
||
|
a string to be displayed.
|
||
|
|
||
|
textprops : dictionary, optional, default: None
|
||
|
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 = {}
|
||
|
|
||
|
if "va" not in textprops:
|
||
|
textprops["va"] = "baseline"
|
||
|
|
||
|
self._text = mtext.Text(0, 0, s, **textprops)
|
||
|
|
||
|
OffsetBox.__init__(self)
|
||
|
|
||
|
self._children = [self._text]
|
||
|
|
||
|
self.offset_transform = mtransforms.Affine2D()
|
||
|
self.offset_transform.clear()
|
||
|
self.offset_transform.translate(0, 0)
|
||
|
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):
|
||
|
"Returns 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.
|
||
|
"""
|
||
|
pass
|
||
|
|
||
|
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):
|
||
|
'''
|
||
|
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)
|
||
|
|
||
|
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):
|
||
|
"""
|
||
|
Draw the children
|
||
|
"""
|
||
|
|
||
|
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
|
||
|
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.
|
||
|
"""
|
||
|
def __init__(self, aux_transform):
|
||
|
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()
|
||
|
|
||
|
def add_artist(self, a):
|
||
|
'Add any :class:`~matplotlib.artist.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.
|
||
|
"""
|
||
|
pass
|
||
|
|
||
|
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):
|
||
|
'''
|
||
|
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)
|
||
|
|
||
|
def get_extent(self, renderer):
|
||
|
|
||
|
# 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_transform
|
||
|
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.
|
||
|
|
||
|
def draw(self, renderer):
|
||
|
"""
|
||
|
Draw the children
|
||
|
"""
|
||
|
|
||
|
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 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.
|
||
|
"""
|
||
|
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):
|
||
|
"""
|
||
|
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
|
||
|
|
||
|
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 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
|
||
|
|
||
|
def get_bbox_to_anchor(self):
|
||
|
"""
|
||
|
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)
|
||
|
|
||
|
def set_bbox_to_anchor(self, bbox, transform=None):
|
||
|
"""
|
||
|
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
|
||
|
|
||
|
def get_window_extent(self, renderer):
|
||
|
'''
|
||
|
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)
|
||
|
|
||
|
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):
|
||
|
"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
|
||
|
|
||
|
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 : 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):
|
||
|
cbook.warn_deprecated(
|
||
|
"3.1", message="Mixing horizontalalignment or "
|
||
|
"verticalalignment with AnchoredText is not supported, "
|
||
|
"deprecated since %(version)s, and will raise an exception "
|
||
|
"%(removal)s.")
|
||
|
|
||
|
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=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)
|
||
|
|
||
|
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 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])
|
||
|
|
||
|
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):
|
||
|
'''
|
||
|
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)
|
||
|
|
||
|
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):
|
||
|
"""
|
||
|
Draw the children
|
||
|
"""
|
||
|
self.image.draw(renderer)
|
||
|
# bbox_artist(self, renderer, fill=False, props=dict(pad=0.))
|
||
|
self.stale = False
|
||
|
|
||
|
|
||
|
class AnnotationBbox(martist.Artist, _AnnotationBase):
|
||
|
"""
|
||
|
Annotation-like class, 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, # 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._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
|
||
|
|
||
|
@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, event):
|
||
|
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
|
||
|
|
||
|
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 fontsize in points
|
||
|
"""
|
||
|
if s is None:
|
||
|
s = rcParams["legend.fontsize"]
|
||
|
|
||
|
self.prop = FontProperties(size=s)
|
||
|
self.stale = True
|
||
|
|
||
|
def get_fontsize(self, s=None):
|
||
|
"""
|
||
|
return fontsize in points
|
||
|
"""
|
||
|
return self.prop.get_size_in_points()
|
||
|
|
||
|
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):
|
||
|
"""
|
||
|
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
|
||
|
|
||
|
|
||
|
class DraggableBase(object):
|
||
|
"""
|
||
|
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 methods::
|
||
|
|
||
|
def artist_picker(self, artist, evt):
|
||
|
'''The picker method that will be used.'''
|
||
|
return self.ref_artist.contains(evt)
|
||
|
|
||
|
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)
|
||
|
|
||
|
ref_artist.set_picker(self.artist_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)
|
||
|
self.canvas.draw()
|
||
|
|
||
|
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(self.ref_artist.figure.bbox)
|
||
|
|
||
|
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.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()
|
||
|
|
||
|
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)
|
||
|
|
||
|
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))
|