1861 lines
62 KiB
Python
1861 lines
62 KiB
Python
from collections import namedtuple
|
|
import contextlib
|
|
from functools import cache, wraps
|
|
import inspect
|
|
from inspect import Signature, Parameter
|
|
import logging
|
|
from numbers import Number, Real
|
|
import re
|
|
import warnings
|
|
|
|
import numpy as np
|
|
|
|
import matplotlib as mpl
|
|
from . import _api, cbook
|
|
from .colors import BoundaryNorm
|
|
from .cm import ScalarMappable
|
|
from .path import Path
|
|
from .transforms import (BboxBase, Bbox, IdentityTransform, Transform, TransformedBbox,
|
|
TransformedPatchPath, TransformedPath)
|
|
|
|
_log = logging.getLogger(__name__)
|
|
|
|
|
|
def _prevent_rasterization(draw):
|
|
# We assume that by default artists are not allowed to rasterize (unless
|
|
# its draw method is explicitly decorated). If it is being drawn after a
|
|
# rasterized artist and it has reached a raster_depth of 0, we stop
|
|
# rasterization so that it does not affect the behavior of normal artist
|
|
# (e.g., change in dpi).
|
|
|
|
@wraps(draw)
|
|
def draw_wrapper(artist, renderer, *args, **kwargs):
|
|
if renderer._raster_depth == 0 and renderer._rasterizing:
|
|
# Only stop when we are not in a rasterized parent
|
|
# and something has been rasterized since last stop.
|
|
renderer.stop_rasterizing()
|
|
renderer._rasterizing = False
|
|
|
|
return draw(artist, renderer, *args, **kwargs)
|
|
|
|
draw_wrapper._supports_rasterization = False
|
|
return draw_wrapper
|
|
|
|
|
|
def allow_rasterization(draw):
|
|
"""
|
|
Decorator for Artist.draw method. Provides routines
|
|
that run before and after the draw call. The before and after functions
|
|
are useful for changing artist-dependent renderer attributes or making
|
|
other setup function calls, such as starting and flushing a mixed-mode
|
|
renderer.
|
|
"""
|
|
|
|
@wraps(draw)
|
|
def draw_wrapper(artist, renderer):
|
|
try:
|
|
if artist.get_rasterized():
|
|
if renderer._raster_depth == 0 and not renderer._rasterizing:
|
|
renderer.start_rasterizing()
|
|
renderer._rasterizing = True
|
|
renderer._raster_depth += 1
|
|
else:
|
|
if renderer._raster_depth == 0 and renderer._rasterizing:
|
|
# Only stop when we are not in a rasterized parent
|
|
# and something has be rasterized since last stop
|
|
renderer.stop_rasterizing()
|
|
renderer._rasterizing = False
|
|
|
|
if artist.get_agg_filter() is not None:
|
|
renderer.start_filter()
|
|
|
|
return draw(artist, renderer)
|
|
finally:
|
|
if artist.get_agg_filter() is not None:
|
|
renderer.stop_filter(artist.get_agg_filter())
|
|
if artist.get_rasterized():
|
|
renderer._raster_depth -= 1
|
|
if (renderer._rasterizing and artist.figure and
|
|
artist.figure.suppressComposite):
|
|
# restart rasterizing to prevent merging
|
|
renderer.stop_rasterizing()
|
|
renderer.start_rasterizing()
|
|
|
|
draw_wrapper._supports_rasterization = True
|
|
return draw_wrapper
|
|
|
|
|
|
def _finalize_rasterization(draw):
|
|
"""
|
|
Decorator for Artist.draw method. Needed on the outermost artist, i.e.
|
|
Figure, to finish up if the render is still in rasterized mode.
|
|
"""
|
|
@wraps(draw)
|
|
def draw_wrapper(artist, renderer, *args, **kwargs):
|
|
result = draw(artist, renderer, *args, **kwargs)
|
|
if renderer._rasterizing:
|
|
renderer.stop_rasterizing()
|
|
renderer._rasterizing = False
|
|
return result
|
|
return draw_wrapper
|
|
|
|
|
|
def _stale_axes_callback(self, val):
|
|
if self.axes:
|
|
self.axes.stale = val
|
|
|
|
|
|
_XYPair = namedtuple("_XYPair", "x y")
|
|
|
|
|
|
class _Unset:
|
|
def __repr__(self):
|
|
return "<UNSET>"
|
|
_UNSET = _Unset()
|
|
|
|
|
|
class Artist:
|
|
"""
|
|
Abstract base class for objects that render into a FigureCanvas.
|
|
|
|
Typically, all visible elements in a figure are subclasses of Artist.
|
|
"""
|
|
|
|
zorder = 0
|
|
|
|
def __init_subclass__(cls):
|
|
|
|
# Decorate draw() method so that all artists are able to stop
|
|
# rastrization when necessary. If the artist's draw method is already
|
|
# decorated (has a `_supports_rasterization` attribute), it won't be
|
|
# decorated.
|
|
|
|
if not hasattr(cls.draw, "_supports_rasterization"):
|
|
cls.draw = _prevent_rasterization(cls.draw)
|
|
|
|
# Inject custom set() methods into the subclass with signature and
|
|
# docstring based on the subclasses' properties.
|
|
|
|
if not hasattr(cls.set, '_autogenerated_signature'):
|
|
# Don't overwrite cls.set if the subclass or one of its parents
|
|
# has defined a set method set itself.
|
|
# If there was no explicit definition, cls.set is inherited from
|
|
# the hierarchy of auto-generated set methods, which hold the
|
|
# flag _autogenerated_signature.
|
|
return
|
|
|
|
cls.set = lambda self, **kwargs: Artist.set(self, **kwargs)
|
|
cls.set.__name__ = "set"
|
|
cls.set.__qualname__ = f"{cls.__qualname__}.set"
|
|
cls._update_set_signature_and_docstring()
|
|
|
|
_PROPERTIES_EXCLUDED_FROM_SET = [
|
|
'navigate_mode', # not a user-facing function
|
|
'figure', # changing the figure is such a profound operation
|
|
# that we don't want this in set()
|
|
'3d_properties', # cannot be used as a keyword due to leading digit
|
|
]
|
|
|
|
@classmethod
|
|
def _update_set_signature_and_docstring(cls):
|
|
"""
|
|
Update the signature of the set function to list all properties
|
|
as keyword arguments.
|
|
|
|
Property aliases are not listed in the signature for brevity, but
|
|
are still accepted as keyword arguments.
|
|
"""
|
|
cls.set.__signature__ = Signature(
|
|
[Parameter("self", Parameter.POSITIONAL_OR_KEYWORD),
|
|
*[Parameter(prop, Parameter.KEYWORD_ONLY, default=_UNSET)
|
|
for prop in ArtistInspector(cls).get_setters()
|
|
if prop not in Artist._PROPERTIES_EXCLUDED_FROM_SET]])
|
|
cls.set._autogenerated_signature = True
|
|
|
|
cls.set.__doc__ = (
|
|
"Set multiple properties at once.\n\n"
|
|
"Supported properties are\n\n"
|
|
+ kwdoc(cls))
|
|
|
|
def __init__(self):
|
|
self._stale = True
|
|
self.stale_callback = None
|
|
self._axes = None
|
|
self.figure = None
|
|
|
|
self._transform = None
|
|
self._transformSet = False
|
|
self._visible = True
|
|
self._animated = False
|
|
self._alpha = None
|
|
self.clipbox = None
|
|
self._clippath = None
|
|
self._clipon = True
|
|
self._label = ''
|
|
self._picker = None
|
|
self._rasterized = False
|
|
self._agg_filter = None
|
|
# Normally, artist classes need to be queried for mouseover info if and
|
|
# only if they override get_cursor_data.
|
|
self._mouseover = type(self).get_cursor_data != Artist.get_cursor_data
|
|
self._callbacks = cbook.CallbackRegistry(signals=["pchanged"])
|
|
try:
|
|
self.axes = None
|
|
except AttributeError:
|
|
# Handle self.axes as a read-only property, as in Figure.
|
|
pass
|
|
self._remove_method = None
|
|
self._url = None
|
|
self._gid = None
|
|
self._snap = None
|
|
self._sketch = mpl.rcParams['path.sketch']
|
|
self._path_effects = mpl.rcParams['path.effects']
|
|
self._sticky_edges = _XYPair([], [])
|
|
self._in_layout = True
|
|
|
|
def __getstate__(self):
|
|
d = self.__dict__.copy()
|
|
d['stale_callback'] = None
|
|
return d
|
|
|
|
def remove(self):
|
|
"""
|
|
Remove the artist from the figure if possible.
|
|
|
|
The effect will not be visible until the figure is redrawn, e.g.,
|
|
with `.FigureCanvasBase.draw_idle`. Call `~.axes.Axes.relim` to
|
|
update the Axes limits if desired.
|
|
|
|
Note: `~.axes.Axes.relim` will not see collections even if the
|
|
collection was added to the Axes with *autolim* = True.
|
|
|
|
Note: there is no support for removing the artist's legend entry.
|
|
"""
|
|
|
|
# There is no method to set the callback. Instead, the parent should
|
|
# set the _remove_method attribute directly. This would be a
|
|
# protected attribute if Python supported that sort of thing. The
|
|
# callback has one parameter, which is the child to be removed.
|
|
if self._remove_method is not None:
|
|
self._remove_method(self)
|
|
# clear stale callback
|
|
self.stale_callback = None
|
|
_ax_flag = False
|
|
if hasattr(self, 'axes') and self.axes:
|
|
# remove from the mouse hit list
|
|
self.axes._mouseover_set.discard(self)
|
|
self.axes.stale = True
|
|
self.axes = None # decouple the artist from the Axes
|
|
_ax_flag = True
|
|
|
|
if self.figure:
|
|
if not _ax_flag:
|
|
self.figure.stale = True
|
|
self.figure = None
|
|
|
|
else:
|
|
raise NotImplementedError('cannot remove artist')
|
|
# TODO: the fix for the collections relim problem is to move the
|
|
# limits calculation into the artist itself, including the property of
|
|
# whether or not the artist should affect the limits. Then there will
|
|
# be no distinction between axes.add_line, axes.add_patch, etc.
|
|
# TODO: add legend support
|
|
|
|
def have_units(self):
|
|
"""Return whether units are set on any axis."""
|
|
ax = self.axes
|
|
return ax and any(axis.have_units() for axis in ax._axis_map.values())
|
|
|
|
def convert_xunits(self, x):
|
|
"""
|
|
Convert *x* using the unit type of the xaxis.
|
|
|
|
If the artist is not contained in an Axes or if the xaxis does not
|
|
have units, *x* itself is returned.
|
|
"""
|
|
ax = getattr(self, 'axes', None)
|
|
if ax is None or ax.xaxis is None:
|
|
return x
|
|
return ax.xaxis.convert_units(x)
|
|
|
|
def convert_yunits(self, y):
|
|
"""
|
|
Convert *y* using the unit type of the yaxis.
|
|
|
|
If the artist is not contained in an Axes or if the yaxis does not
|
|
have units, *y* itself is returned.
|
|
"""
|
|
ax = getattr(self, 'axes', None)
|
|
if ax is None or ax.yaxis is None:
|
|
return y
|
|
return ax.yaxis.convert_units(y)
|
|
|
|
@property
|
|
def axes(self):
|
|
"""The `~.axes.Axes` instance the artist resides in, or *None*."""
|
|
return self._axes
|
|
|
|
@axes.setter
|
|
def axes(self, new_axes):
|
|
if (new_axes is not None and self._axes is not None
|
|
and new_axes != self._axes):
|
|
raise ValueError("Can not reset the Axes. You are probably trying to reuse "
|
|
"an artist in more than one Axes which is not supported")
|
|
self._axes = new_axes
|
|
if new_axes is not None and new_axes is not self:
|
|
self.stale_callback = _stale_axes_callback
|
|
|
|
@property
|
|
def stale(self):
|
|
"""
|
|
Whether the artist is 'stale' and needs to be re-drawn for the output
|
|
to match the internal state of the artist.
|
|
"""
|
|
return self._stale
|
|
|
|
@stale.setter
|
|
def stale(self, val):
|
|
self._stale = val
|
|
|
|
# if the artist is animated it does not take normal part in the
|
|
# draw stack and is not expected to be drawn as part of the normal
|
|
# draw loop (when not saving) so do not propagate this change
|
|
if self._animated:
|
|
return
|
|
|
|
if val and self.stale_callback is not None:
|
|
self.stale_callback(self, val)
|
|
|
|
def get_window_extent(self, renderer=None):
|
|
"""
|
|
Get the artist's bounding box in display space.
|
|
|
|
The bounding box' width and height are nonnegative.
|
|
|
|
Subclasses should override for inclusion in the bounding box
|
|
"tight" calculation. Default is to return an empty bounding
|
|
box at 0, 0.
|
|
|
|
Be careful when using this function, the results will not update
|
|
if the artist window extent of the artist changes. The extent
|
|
can change due to any changes in the transform stack, such as
|
|
changing the Axes limits, the figure size, or the canvas used
|
|
(as is done when saving a figure). This can lead to unexpected
|
|
behavior where interactive figures will look fine on the screen,
|
|
but will save incorrectly.
|
|
"""
|
|
return Bbox([[0, 0], [0, 0]])
|
|
|
|
def get_tightbbox(self, renderer=None):
|
|
"""
|
|
Like `.Artist.get_window_extent`, but includes any clipping.
|
|
|
|
Parameters
|
|
----------
|
|
renderer : `~matplotlib.backend_bases.RendererBase` subclass, optional
|
|
renderer that will be used to draw the figures (i.e.
|
|
``fig.canvas.get_renderer()``)
|
|
|
|
Returns
|
|
-------
|
|
`.Bbox` or None
|
|
The enclosing bounding box (in figure pixel coordinates).
|
|
Returns None if clipping results in no intersection.
|
|
"""
|
|
bbox = self.get_window_extent(renderer)
|
|
if self.get_clip_on():
|
|
clip_box = self.get_clip_box()
|
|
if clip_box is not None:
|
|
bbox = Bbox.intersection(bbox, clip_box)
|
|
clip_path = self.get_clip_path()
|
|
if clip_path is not None and bbox is not None:
|
|
clip_path = clip_path.get_fully_transformed_path()
|
|
bbox = Bbox.intersection(bbox, clip_path.get_extents())
|
|
return bbox
|
|
|
|
def add_callback(self, func):
|
|
"""
|
|
Add a callback function that will be called whenever one of the
|
|
`.Artist`'s properties changes.
|
|
|
|
Parameters
|
|
----------
|
|
func : callable
|
|
The callback function. It must have the signature::
|
|
|
|
def func(artist: Artist) -> Any
|
|
|
|
where *artist* is the calling `.Artist`. Return values may exist
|
|
but are ignored.
|
|
|
|
Returns
|
|
-------
|
|
int
|
|
The observer id associated with the callback. This id can be
|
|
used for removing the callback with `.remove_callback` later.
|
|
|
|
See Also
|
|
--------
|
|
remove_callback
|
|
"""
|
|
# Wrapping func in a lambda ensures it can be connected multiple times
|
|
# and never gets weakref-gc'ed.
|
|
return self._callbacks.connect("pchanged", lambda: func(self))
|
|
|
|
def remove_callback(self, oid):
|
|
"""
|
|
Remove a callback based on its observer id.
|
|
|
|
See Also
|
|
--------
|
|
add_callback
|
|
"""
|
|
self._callbacks.disconnect(oid)
|
|
|
|
def pchanged(self):
|
|
"""
|
|
Call all of the registered callbacks.
|
|
|
|
This function is triggered internally when a property is changed.
|
|
|
|
See Also
|
|
--------
|
|
add_callback
|
|
remove_callback
|
|
"""
|
|
self._callbacks.process("pchanged")
|
|
|
|
def is_transform_set(self):
|
|
"""
|
|
Return whether the Artist has an explicitly set transform.
|
|
|
|
This is *True* after `.set_transform` has been called.
|
|
"""
|
|
return self._transformSet
|
|
|
|
def set_transform(self, t):
|
|
"""
|
|
Set the artist transform.
|
|
|
|
Parameters
|
|
----------
|
|
t : `~matplotlib.transforms.Transform`
|
|
"""
|
|
self._transform = t
|
|
self._transformSet = True
|
|
self.pchanged()
|
|
self.stale = True
|
|
|
|
def get_transform(self):
|
|
"""Return the `.Transform` instance used by this artist."""
|
|
if self._transform is None:
|
|
self._transform = IdentityTransform()
|
|
elif (not isinstance(self._transform, Transform)
|
|
and hasattr(self._transform, '_as_mpl_transform')):
|
|
self._transform = self._transform._as_mpl_transform(self.axes)
|
|
return self._transform
|
|
|
|
def get_children(self):
|
|
r"""Return a list of the child `.Artist`\s of this `.Artist`."""
|
|
return []
|
|
|
|
def _different_canvas(self, event):
|
|
"""
|
|
Check whether an *event* occurred on a canvas other that this artist's canvas.
|
|
|
|
If this method returns True, the event definitely occurred on a different
|
|
canvas; if it returns False, either it occurred on the same canvas, or we may
|
|
not have enough information to know.
|
|
|
|
Subclasses should start their definition of `contains` as follows::
|
|
|
|
if self._different_canvas(mouseevent):
|
|
return False, {}
|
|
# subclass-specific implementation follows
|
|
"""
|
|
return (getattr(event, "canvas", None) is not None and self.figure is not None
|
|
and event.canvas is not self.figure.canvas)
|
|
|
|
def contains(self, mouseevent):
|
|
"""
|
|
Test whether the artist contains the mouse event.
|
|
|
|
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.
|
|
"""
|
|
_log.warning("%r needs 'contains' method", self.__class__.__name__)
|
|
return False, {}
|
|
|
|
def pickable(self):
|
|
"""
|
|
Return whether the artist is pickable.
|
|
|
|
See Also
|
|
--------
|
|
.Artist.set_picker, .Artist.get_picker, .Artist.pick
|
|
"""
|
|
return self.figure is not None and self._picker is not None
|
|
|
|
def pick(self, mouseevent):
|
|
"""
|
|
Process a pick event.
|
|
|
|
Each child artist will fire a pick event if *mouseevent* is over
|
|
the artist and the artist has picker set.
|
|
|
|
See Also
|
|
--------
|
|
.Artist.set_picker, .Artist.get_picker, .Artist.pickable
|
|
"""
|
|
from .backend_bases import PickEvent # Circular import.
|
|
# Pick self
|
|
if self.pickable():
|
|
picker = self.get_picker()
|
|
if callable(picker):
|
|
inside, prop = picker(self, mouseevent)
|
|
else:
|
|
inside, prop = self.contains(mouseevent)
|
|
if inside:
|
|
PickEvent("pick_event", self.figure.canvas,
|
|
mouseevent, self, **prop)._process()
|
|
|
|
# Pick children
|
|
for a in self.get_children():
|
|
# make sure the event happened in the same Axes
|
|
ax = getattr(a, 'axes', None)
|
|
if (isinstance(a, mpl.figure.SubFigure)
|
|
or mouseevent.inaxes is None or ax is None
|
|
or mouseevent.inaxes == ax):
|
|
# we need to check if mouseevent.inaxes is None
|
|
# because some objects associated with an Axes (e.g., a
|
|
# tick label) can be outside the bounding box of the
|
|
# Axes and inaxes will be None
|
|
# also check that ax is None so that it traverse objects
|
|
# which do not have an axes property but children might
|
|
a.pick(mouseevent)
|
|
|
|
def set_picker(self, picker):
|
|
"""
|
|
Define the picking behavior of the artist.
|
|
|
|
Parameters
|
|
----------
|
|
picker : None or bool or float or callable
|
|
This can be one of the following:
|
|
|
|
- *None*: Picking is disabled for this artist (default).
|
|
|
|
- A boolean: If *True* then picking will be enabled and the
|
|
artist will fire a pick event if the mouse event is over
|
|
the artist.
|
|
|
|
- A float: If picker is a number it is interpreted as an
|
|
epsilon tolerance in points and the artist will fire
|
|
off an event if its data is within epsilon of the mouse
|
|
event. For some artists like lines and patch collections,
|
|
the artist may provide additional data to the pick event
|
|
that is generated, e.g., the indices of the data within
|
|
epsilon of the pick event
|
|
|
|
- A function: If picker is callable, it is a user supplied
|
|
function which determines whether the artist is hit by the
|
|
mouse event::
|
|
|
|
hit, props = picker(artist, mouseevent)
|
|
|
|
to determine the hit test. if the mouse event is over the
|
|
artist, return *hit=True* and props is a dictionary of
|
|
properties you want added to the PickEvent attributes.
|
|
"""
|
|
self._picker = picker
|
|
|
|
def get_picker(self):
|
|
"""
|
|
Return the picking behavior of the artist.
|
|
|
|
The possible values are described in `.Artist.set_picker`.
|
|
|
|
See Also
|
|
--------
|
|
.Artist.set_picker, .Artist.pickable, .Artist.pick
|
|
"""
|
|
return self._picker
|
|
|
|
def get_url(self):
|
|
"""Return the url."""
|
|
return self._url
|
|
|
|
def set_url(self, url):
|
|
"""
|
|
Set the url for the artist.
|
|
|
|
Parameters
|
|
----------
|
|
url : str
|
|
"""
|
|
self._url = url
|
|
|
|
def get_gid(self):
|
|
"""Return the group id."""
|
|
return self._gid
|
|
|
|
def set_gid(self, gid):
|
|
"""
|
|
Set the (group) id for the artist.
|
|
|
|
Parameters
|
|
----------
|
|
gid : str
|
|
"""
|
|
self._gid = gid
|
|
|
|
def get_snap(self):
|
|
"""
|
|
Return the snap setting.
|
|
|
|
See `.set_snap` for details.
|
|
"""
|
|
if mpl.rcParams['path.snap']:
|
|
return self._snap
|
|
else:
|
|
return False
|
|
|
|
def set_snap(self, snap):
|
|
"""
|
|
Set the snapping behavior.
|
|
|
|
Snapping aligns positions with the pixel grid, which results in
|
|
clearer images. For example, if a black line of 1px width was
|
|
defined at a position in between two pixels, the resulting image
|
|
would contain the interpolated value of that line in the pixel grid,
|
|
which would be a grey value on both adjacent pixel positions. In
|
|
contrast, snapping will move the line to the nearest integer pixel
|
|
value, so that the resulting image will really contain a 1px wide
|
|
black line.
|
|
|
|
Snapping is currently only supported by the Agg and MacOSX backends.
|
|
|
|
Parameters
|
|
----------
|
|
snap : bool or None
|
|
Possible values:
|
|
|
|
- *True*: Snap vertices to the nearest pixel center.
|
|
- *False*: Do not modify vertex positions.
|
|
- *None*: (auto) If the path contains only rectilinear line
|
|
segments, round to the nearest pixel center.
|
|
"""
|
|
self._snap = snap
|
|
self.stale = True
|
|
|
|
def get_sketch_params(self):
|
|
"""
|
|
Return the sketch parameters for the artist.
|
|
|
|
Returns
|
|
-------
|
|
tuple or None
|
|
|
|
A 3-tuple with the following elements:
|
|
|
|
- *scale*: The amplitude of the wiggle perpendicular to the
|
|
source line.
|
|
- *length*: The length of the wiggle along the line.
|
|
- *randomness*: The scale factor by which the length is
|
|
shrunken or expanded.
|
|
|
|
Returns *None* if no sketch parameters were set.
|
|
"""
|
|
return self._sketch
|
|
|
|
def set_sketch_params(self, scale=None, length=None, randomness=None):
|
|
"""
|
|
Set the sketch parameters.
|
|
|
|
Parameters
|
|
----------
|
|
scale : float, optional
|
|
The amplitude of the wiggle perpendicular to the source
|
|
line, in pixels. If scale is `None`, or not provided, no
|
|
sketch filter will be provided.
|
|
length : float, optional
|
|
The length of the wiggle along the line, in pixels
|
|
(default 128.0)
|
|
randomness : float, optional
|
|
The scale factor by which the length is shrunken or
|
|
expanded (default 16.0)
|
|
|
|
The PGF backend uses this argument as an RNG seed and not as
|
|
described above. Using the same seed yields the same random shape.
|
|
|
|
.. ACCEPTS: (scale: float, length: float, randomness: float)
|
|
"""
|
|
if scale is None:
|
|
self._sketch = None
|
|
else:
|
|
self._sketch = (scale, length or 128.0, randomness or 16.0)
|
|
self.stale = True
|
|
|
|
def set_path_effects(self, path_effects):
|
|
"""
|
|
Set the path effects.
|
|
|
|
Parameters
|
|
----------
|
|
path_effects : list of `.AbstractPathEffect`
|
|
"""
|
|
self._path_effects = path_effects
|
|
self.stale = True
|
|
|
|
def get_path_effects(self):
|
|
return self._path_effects
|
|
|
|
def get_figure(self):
|
|
"""Return the `.Figure` instance the artist belongs to."""
|
|
return self.figure
|
|
|
|
def set_figure(self, fig):
|
|
"""
|
|
Set the `.Figure` instance the artist belongs to.
|
|
|
|
Parameters
|
|
----------
|
|
fig : `~matplotlib.figure.Figure`
|
|
"""
|
|
# if this is a no-op just return
|
|
if self.figure is fig:
|
|
return
|
|
# if we currently have a figure (the case of both `self.figure`
|
|
# and *fig* being none is taken care of above) we then user is
|
|
# trying to change the figure an artist is associated with which
|
|
# is not allowed for the same reason as adding the same instance
|
|
# to more than one Axes
|
|
if self.figure is not None:
|
|
raise RuntimeError("Can not put single artist in "
|
|
"more than one figure")
|
|
self.figure = fig
|
|
if self.figure and self.figure is not self:
|
|
self.pchanged()
|
|
self.stale = True
|
|
|
|
def set_clip_box(self, clipbox):
|
|
"""
|
|
Set the artist's clip `.Bbox`.
|
|
|
|
Parameters
|
|
----------
|
|
clipbox : `~matplotlib.transforms.BboxBase` or None
|
|
Will typically be created from a `.TransformedBbox`. For instance,
|
|
``TransformedBbox(Bbox([[0, 0], [1, 1]]), ax.transAxes)`` is the default
|
|
clipping for an artist added to an Axes.
|
|
|
|
"""
|
|
_api.check_isinstance((BboxBase, None), clipbox=clipbox)
|
|
if clipbox != self.clipbox:
|
|
self.clipbox = clipbox
|
|
self.pchanged()
|
|
self.stale = True
|
|
|
|
def set_clip_path(self, path, transform=None):
|
|
"""
|
|
Set the artist's clip path.
|
|
|
|
Parameters
|
|
----------
|
|
path : `~matplotlib.patches.Patch` or `.Path` or `.TransformedPath` or None
|
|
The clip path. If given a `.Path`, *transform* must be provided as
|
|
well. If *None*, a previously set clip path is removed.
|
|
transform : `~matplotlib.transforms.Transform`, optional
|
|
Only used if *path* is a `.Path`, in which case the given `.Path`
|
|
is converted to a `.TransformedPath` using *transform*.
|
|
|
|
Notes
|
|
-----
|
|
For efficiency, if *path* is a `.Rectangle` this method will set the
|
|
clipping box to the corresponding rectangle and set the clipping path
|
|
to ``None``.
|
|
|
|
For technical reasons (support of `~.Artist.set`), a tuple
|
|
(*path*, *transform*) is also accepted as a single positional
|
|
parameter.
|
|
|
|
.. ACCEPTS: Patch or (Path, Transform) or None
|
|
"""
|
|
from matplotlib.patches import Patch, Rectangle
|
|
|
|
success = False
|
|
if transform is None:
|
|
if isinstance(path, Rectangle):
|
|
self.clipbox = TransformedBbox(Bbox.unit(),
|
|
path.get_transform())
|
|
self._clippath = None
|
|
success = True
|
|
elif isinstance(path, Patch):
|
|
self._clippath = TransformedPatchPath(path)
|
|
success = True
|
|
elif isinstance(path, tuple):
|
|
path, transform = path
|
|
|
|
if path is None:
|
|
self._clippath = None
|
|
success = True
|
|
elif isinstance(path, Path):
|
|
self._clippath = TransformedPath(path, transform)
|
|
success = True
|
|
elif isinstance(path, TransformedPatchPath):
|
|
self._clippath = path
|
|
success = True
|
|
elif isinstance(path, TransformedPath):
|
|
self._clippath = path
|
|
success = True
|
|
|
|
if not success:
|
|
raise TypeError(
|
|
"Invalid arguments to set_clip_path, of type "
|
|
f"{type(path).__name__} and {type(transform).__name__}")
|
|
# This may result in the callbacks being hit twice, but guarantees they
|
|
# will be hit at least once.
|
|
self.pchanged()
|
|
self.stale = True
|
|
|
|
def get_alpha(self):
|
|
"""
|
|
Return the alpha value used for blending - not supported on all
|
|
backends.
|
|
"""
|
|
return self._alpha
|
|
|
|
def get_visible(self):
|
|
"""Return the visibility."""
|
|
return self._visible
|
|
|
|
def get_animated(self):
|
|
"""Return whether the artist is animated."""
|
|
return self._animated
|
|
|
|
def get_in_layout(self):
|
|
"""
|
|
Return boolean flag, ``True`` if artist is included in layout
|
|
calculations.
|
|
|
|
E.g. :ref:`constrainedlayout_guide`,
|
|
`.Figure.tight_layout()`, and
|
|
``fig.savefig(fname, bbox_inches='tight')``.
|
|
"""
|
|
return self._in_layout
|
|
|
|
def _fully_clipped_to_axes(self):
|
|
"""
|
|
Return a boolean flag, ``True`` if the artist is clipped to the Axes
|
|
and can thus be skipped in layout calculations. Requires `get_clip_on`
|
|
is True, one of `clip_box` or `clip_path` is set, ``clip_box.extents``
|
|
is equivalent to ``ax.bbox.extents`` (if set), and ``clip_path._patch``
|
|
is equivalent to ``ax.patch`` (if set).
|
|
"""
|
|
# Note that ``clip_path.get_fully_transformed_path().get_extents()``
|
|
# cannot be directly compared to ``axes.bbox.extents`` because the
|
|
# extents may be undefined (i.e. equivalent to ``Bbox.null()``)
|
|
# before the associated artist is drawn, and this method is meant
|
|
# to determine whether ``axes.get_tightbbox()`` may bypass drawing
|
|
clip_box = self.get_clip_box()
|
|
clip_path = self.get_clip_path()
|
|
return (self.axes is not None
|
|
and self.get_clip_on()
|
|
and (clip_box is not None or clip_path is not None)
|
|
and (clip_box is None
|
|
or np.all(clip_box.extents == self.axes.bbox.extents))
|
|
and (clip_path is None
|
|
or isinstance(clip_path, TransformedPatchPath)
|
|
and clip_path._patch is self.axes.patch))
|
|
|
|
def get_clip_on(self):
|
|
"""Return whether the artist uses clipping."""
|
|
return self._clipon
|
|
|
|
def get_clip_box(self):
|
|
"""Return the clipbox."""
|
|
return self.clipbox
|
|
|
|
def get_clip_path(self):
|
|
"""Return the clip path."""
|
|
return self._clippath
|
|
|
|
def get_transformed_clip_path_and_affine(self):
|
|
"""
|
|
Return the clip path with the non-affine part of its
|
|
transformation applied, and the remaining affine part of its
|
|
transformation.
|
|
"""
|
|
if self._clippath is not None:
|
|
return self._clippath.get_transformed_path_and_affine()
|
|
return None, None
|
|
|
|
def set_clip_on(self, b):
|
|
"""
|
|
Set whether the artist uses clipping.
|
|
|
|
When False, artists will be visible outside the Axes which
|
|
can lead to unexpected results.
|
|
|
|
Parameters
|
|
----------
|
|
b : bool
|
|
"""
|
|
self._clipon = b
|
|
# This may result in the callbacks being hit twice, but ensures they
|
|
# are hit at least once
|
|
self.pchanged()
|
|
self.stale = True
|
|
|
|
def _set_gc_clip(self, gc):
|
|
"""Set the clip properly for the gc."""
|
|
if self._clipon:
|
|
if self.clipbox is not None:
|
|
gc.set_clip_rectangle(self.clipbox)
|
|
gc.set_clip_path(self._clippath)
|
|
else:
|
|
gc.set_clip_rectangle(None)
|
|
gc.set_clip_path(None)
|
|
|
|
def get_rasterized(self):
|
|
"""Return whether the artist is to be rasterized."""
|
|
return self._rasterized
|
|
|
|
def set_rasterized(self, rasterized):
|
|
"""
|
|
Force rasterized (bitmap) drawing for vector graphics output.
|
|
|
|
Rasterized drawing is not supported by all artists. If you try to
|
|
enable this on an artist that does not support it, the command has no
|
|
effect and a warning will be issued.
|
|
|
|
This setting is ignored for pixel-based output.
|
|
|
|
See also :doc:`/gallery/misc/rasterization_demo`.
|
|
|
|
Parameters
|
|
----------
|
|
rasterized : bool
|
|
"""
|
|
supports_rasterization = getattr(self.draw,
|
|
"_supports_rasterization", False)
|
|
if rasterized and not supports_rasterization:
|
|
_api.warn_external(f"Rasterization of '{self}' will be ignored")
|
|
|
|
self._rasterized = rasterized
|
|
|
|
def get_agg_filter(self):
|
|
"""Return filter function to be used for agg filter."""
|
|
return self._agg_filter
|
|
|
|
def set_agg_filter(self, filter_func):
|
|
"""
|
|
Set the agg filter.
|
|
|
|
Parameters
|
|
----------
|
|
filter_func : callable
|
|
A filter function, which takes a (m, n, depth) float array
|
|
and a dpi value, and returns a (m, n, depth) array and two
|
|
offsets from the bottom left corner of the image
|
|
|
|
.. ACCEPTS: a filter function, which takes a (m, n, 3) float array
|
|
and a dpi value, and returns a (m, n, 3) array and two offsets
|
|
from the bottom left corner of the image
|
|
"""
|
|
self._agg_filter = filter_func
|
|
self.stale = True
|
|
|
|
def draw(self, renderer):
|
|
"""
|
|
Draw the Artist (and its children) using the given renderer.
|
|
|
|
This has no effect if the artist is not visible (`.Artist.get_visible`
|
|
returns False).
|
|
|
|
Parameters
|
|
----------
|
|
renderer : `~matplotlib.backend_bases.RendererBase` subclass.
|
|
|
|
Notes
|
|
-----
|
|
This method is overridden in the Artist subclasses.
|
|
"""
|
|
if not self.get_visible():
|
|
return
|
|
self.stale = False
|
|
|
|
def set_alpha(self, alpha):
|
|
"""
|
|
Set the alpha value used for blending - not supported on all backends.
|
|
|
|
Parameters
|
|
----------
|
|
alpha : scalar or None
|
|
*alpha* must be within the 0-1 range, inclusive.
|
|
"""
|
|
if alpha is not None and not isinstance(alpha, Real):
|
|
raise TypeError(
|
|
f'alpha must be numeric or None, not {type(alpha)}')
|
|
if alpha is not None and not (0 <= alpha <= 1):
|
|
raise ValueError(f'alpha ({alpha}) is outside 0-1 range')
|
|
if alpha != self._alpha:
|
|
self._alpha = alpha
|
|
self.pchanged()
|
|
self.stale = True
|
|
|
|
def _set_alpha_for_array(self, alpha):
|
|
"""
|
|
Set the alpha value used for blending - not supported on all backends.
|
|
|
|
Parameters
|
|
----------
|
|
alpha : array-like or scalar or None
|
|
All values must be within the 0-1 range, inclusive.
|
|
Masked values and nans are not supported.
|
|
"""
|
|
if isinstance(alpha, str):
|
|
raise TypeError("alpha must be numeric or None, not a string")
|
|
if not np.iterable(alpha):
|
|
Artist.set_alpha(self, alpha)
|
|
return
|
|
alpha = np.asarray(alpha)
|
|
if not (0 <= alpha.min() and alpha.max() <= 1):
|
|
raise ValueError('alpha must be between 0 and 1, inclusive, '
|
|
f'but min is {alpha.min()}, max is {alpha.max()}')
|
|
self._alpha = alpha
|
|
self.pchanged()
|
|
self.stale = True
|
|
|
|
def set_visible(self, b):
|
|
"""
|
|
Set the artist's visibility.
|
|
|
|
Parameters
|
|
----------
|
|
b : bool
|
|
"""
|
|
if b != self._visible:
|
|
self._visible = b
|
|
self.pchanged()
|
|
self.stale = True
|
|
|
|
def set_animated(self, b):
|
|
"""
|
|
Set whether the artist is intended to be used in an animation.
|
|
|
|
If True, the artist is excluded from regular drawing of the figure.
|
|
You have to call `.Figure.draw_artist` / `.Axes.draw_artist`
|
|
explicitly on the artist. This approach is used to speed up animations
|
|
using blitting.
|
|
|
|
See also `matplotlib.animation` and
|
|
:ref:`blitting`.
|
|
|
|
Parameters
|
|
----------
|
|
b : bool
|
|
"""
|
|
if self._animated != b:
|
|
self._animated = b
|
|
self.pchanged()
|
|
|
|
def set_in_layout(self, in_layout):
|
|
"""
|
|
Set if artist is to be included in layout calculations,
|
|
E.g. :ref:`constrainedlayout_guide`,
|
|
`.Figure.tight_layout()`, and
|
|
``fig.savefig(fname, bbox_inches='tight')``.
|
|
|
|
Parameters
|
|
----------
|
|
in_layout : bool
|
|
"""
|
|
self._in_layout = in_layout
|
|
|
|
def get_label(self):
|
|
"""Return the label used for this artist in the legend."""
|
|
return self._label
|
|
|
|
def set_label(self, s):
|
|
"""
|
|
Set a label that will be displayed in the legend.
|
|
|
|
Parameters
|
|
----------
|
|
s : object
|
|
*s* will be converted to a string by calling `str`.
|
|
"""
|
|
label = str(s) if s is not None else None
|
|
if label != self._label:
|
|
self._label = label
|
|
self.pchanged()
|
|
self.stale = True
|
|
|
|
def get_zorder(self):
|
|
"""Return the artist's zorder."""
|
|
return self.zorder
|
|
|
|
def set_zorder(self, level):
|
|
"""
|
|
Set the zorder for the artist. Artists with lower zorder
|
|
values are drawn first.
|
|
|
|
Parameters
|
|
----------
|
|
level : float
|
|
"""
|
|
if level is None:
|
|
level = self.__class__.zorder
|
|
if level != self.zorder:
|
|
self.zorder = level
|
|
self.pchanged()
|
|
self.stale = True
|
|
|
|
@property
|
|
def sticky_edges(self):
|
|
"""
|
|
``x`` and ``y`` sticky edge lists for autoscaling.
|
|
|
|
When performing autoscaling, if a data limit coincides with a value in
|
|
the corresponding sticky_edges list, then no margin will be added--the
|
|
view limit "sticks" to the edge. A typical use case is histograms,
|
|
where one usually expects no margin on the bottom edge (0) of the
|
|
histogram.
|
|
|
|
Moreover, margin expansion "bumps" against sticky edges and cannot
|
|
cross them. For example, if the upper data limit is 1.0, the upper
|
|
view limit computed by simple margin application is 1.2, but there is a
|
|
sticky edge at 1.1, then the actual upper view limit will be 1.1.
|
|
|
|
This attribute cannot be assigned to; however, the ``x`` and ``y``
|
|
lists can be modified in place as needed.
|
|
|
|
Examples
|
|
--------
|
|
>>> artist.sticky_edges.x[:] = (xmin, xmax)
|
|
>>> artist.sticky_edges.y[:] = (ymin, ymax)
|
|
|
|
"""
|
|
return self._sticky_edges
|
|
|
|
def update_from(self, other):
|
|
"""Copy properties from *other* to *self*."""
|
|
self._transform = other._transform
|
|
self._transformSet = other._transformSet
|
|
self._visible = other._visible
|
|
self._alpha = other._alpha
|
|
self.clipbox = other.clipbox
|
|
self._clipon = other._clipon
|
|
self._clippath = other._clippath
|
|
self._label = other._label
|
|
self._sketch = other._sketch
|
|
self._path_effects = other._path_effects
|
|
self.sticky_edges.x[:] = other.sticky_edges.x.copy()
|
|
self.sticky_edges.y[:] = other.sticky_edges.y.copy()
|
|
self.pchanged()
|
|
self.stale = True
|
|
|
|
def properties(self):
|
|
"""Return a dictionary of all the properties of the artist."""
|
|
return ArtistInspector(self).properties()
|
|
|
|
def _update_props(self, props, errfmt):
|
|
"""
|
|
Helper for `.Artist.set` and `.Artist.update`.
|
|
|
|
*errfmt* is used to generate error messages for invalid property
|
|
names; it gets formatted with ``type(self)`` and the property name.
|
|
"""
|
|
ret = []
|
|
with cbook._setattr_cm(self, eventson=False):
|
|
for k, v in props.items():
|
|
# Allow attributes we want to be able to update through
|
|
# art.update, art.set, setp.
|
|
if k == "axes":
|
|
ret.append(setattr(self, k, v))
|
|
else:
|
|
func = getattr(self, f"set_{k}", None)
|
|
if not callable(func):
|
|
raise AttributeError(
|
|
errfmt.format(cls=type(self), prop_name=k))
|
|
ret.append(func(v))
|
|
if ret:
|
|
self.pchanged()
|
|
self.stale = True
|
|
return ret
|
|
|
|
def update(self, props):
|
|
"""
|
|
Update this artist's properties from the dict *props*.
|
|
|
|
Parameters
|
|
----------
|
|
props : dict
|
|
"""
|
|
return self._update_props(
|
|
props, "{cls.__name__!r} object has no property {prop_name!r}")
|
|
|
|
def _internal_update(self, kwargs):
|
|
"""
|
|
Update artist properties without prenormalizing them, but generating
|
|
errors as if calling `set`.
|
|
|
|
The lack of prenormalization is to maintain backcompatibility.
|
|
"""
|
|
return self._update_props(
|
|
kwargs, "{cls.__name__}.set() got an unexpected keyword argument "
|
|
"{prop_name!r}")
|
|
|
|
def set(self, **kwargs):
|
|
# docstring and signature are auto-generated via
|
|
# Artist._update_set_signature_and_docstring() at the end of the
|
|
# module.
|
|
return self._internal_update(cbook.normalize_kwargs(kwargs, self))
|
|
|
|
@contextlib.contextmanager
|
|
def _cm_set(self, **kwargs):
|
|
"""
|
|
`.Artist.set` context-manager that restores original values at exit.
|
|
"""
|
|
orig_vals = {k: getattr(self, f"get_{k}")() for k in kwargs}
|
|
try:
|
|
self.set(**kwargs)
|
|
yield
|
|
finally:
|
|
self.set(**orig_vals)
|
|
|
|
def findobj(self, match=None, include_self=True):
|
|
"""
|
|
Find artist objects.
|
|
|
|
Recursively find all `.Artist` instances contained in the artist.
|
|
|
|
Parameters
|
|
----------
|
|
match
|
|
A filter criterion for the matches. This can be
|
|
|
|
- *None*: Return all objects contained in artist.
|
|
- A function with signature ``def match(artist: Artist) -> bool``.
|
|
The result will only contain artists for which the function
|
|
returns *True*.
|
|
- A class instance: e.g., `.Line2D`. The result will only contain
|
|
artists of this class or its subclasses (``isinstance`` check).
|
|
|
|
include_self : bool
|
|
Include *self* in the list to be checked for a match.
|
|
|
|
Returns
|
|
-------
|
|
list of `.Artist`
|
|
|
|
"""
|
|
if match is None: # always return True
|
|
def matchfunc(x):
|
|
return True
|
|
elif isinstance(match, type) and issubclass(match, Artist):
|
|
def matchfunc(x):
|
|
return isinstance(x, match)
|
|
elif callable(match):
|
|
matchfunc = match
|
|
else:
|
|
raise ValueError('match must be None, a matplotlib.artist.Artist '
|
|
'subclass, or a callable')
|
|
|
|
artists = sum([c.findobj(matchfunc) for c in self.get_children()], [])
|
|
if include_self and matchfunc(self):
|
|
artists.append(self)
|
|
return artists
|
|
|
|
def get_cursor_data(self, event):
|
|
"""
|
|
Return the cursor data for a given event.
|
|
|
|
.. note::
|
|
This method is intended to be overridden by artist subclasses.
|
|
As an end-user of Matplotlib you will most likely not call this
|
|
method yourself.
|
|
|
|
Cursor data can be used by Artists to provide additional context
|
|
information for a given event. The default implementation just returns
|
|
*None*.
|
|
|
|
Subclasses can override the method and return arbitrary data. However,
|
|
when doing so, they must ensure that `.format_cursor_data` can convert
|
|
the data to a string representation.
|
|
|
|
The only current use case is displaying the z-value of an `.AxesImage`
|
|
in the status bar of a plot window, while moving the mouse.
|
|
|
|
Parameters
|
|
----------
|
|
event : `~matplotlib.backend_bases.MouseEvent`
|
|
|
|
See Also
|
|
--------
|
|
format_cursor_data
|
|
|
|
"""
|
|
return None
|
|
|
|
def format_cursor_data(self, data):
|
|
"""
|
|
Return a string representation of *data*.
|
|
|
|
.. note::
|
|
This method is intended to be overridden by artist subclasses.
|
|
As an end-user of Matplotlib you will most likely not call this
|
|
method yourself.
|
|
|
|
The default implementation converts ints and floats and arrays of ints
|
|
and floats into a comma-separated string enclosed in square brackets,
|
|
unless the artist has an associated colorbar, in which case scalar
|
|
values are formatted using the colorbar's formatter.
|
|
|
|
See Also
|
|
--------
|
|
get_cursor_data
|
|
"""
|
|
if np.ndim(data) == 0 and isinstance(self, ScalarMappable):
|
|
# This block logically belongs to ScalarMappable, but can't be
|
|
# implemented in it because most ScalarMappable subclasses inherit
|
|
# from Artist first and from ScalarMappable second, so
|
|
# Artist.format_cursor_data would always have precedence over
|
|
# ScalarMappable.format_cursor_data.
|
|
n = self.cmap.N
|
|
if np.ma.getmask(data):
|
|
return "[]"
|
|
normed = self.norm(data)
|
|
if np.isfinite(normed):
|
|
if isinstance(self.norm, BoundaryNorm):
|
|
# not an invertible normalization mapping
|
|
cur_idx = np.argmin(np.abs(self.norm.boundaries - data))
|
|
neigh_idx = max(0, cur_idx - 1)
|
|
# use max diff to prevent delta == 0
|
|
delta = np.diff(
|
|
self.norm.boundaries[neigh_idx:cur_idx + 2]
|
|
).max()
|
|
|
|
else:
|
|
# Midpoints of neighboring color intervals.
|
|
neighbors = self.norm.inverse(
|
|
(int(normed * n) + np.array([0, 1])) / n)
|
|
delta = abs(neighbors - data).max()
|
|
g_sig_digits = cbook._g_sig_digits(data, delta)
|
|
else:
|
|
g_sig_digits = 3 # Consistent with default below.
|
|
return f"[{data:-#.{g_sig_digits}g}]"
|
|
else:
|
|
try:
|
|
data[0]
|
|
except (TypeError, IndexError):
|
|
data = [data]
|
|
data_str = ', '.join(f'{item:0.3g}' for item in data
|
|
if isinstance(item, Number))
|
|
return "[" + data_str + "]"
|
|
|
|
def get_mouseover(self):
|
|
"""
|
|
Return whether this artist is queried for custom context information
|
|
when the mouse cursor moves over it.
|
|
"""
|
|
return self._mouseover
|
|
|
|
def set_mouseover(self, mouseover):
|
|
"""
|
|
Set whether this artist is queried for custom context information when
|
|
the mouse cursor moves over it.
|
|
|
|
Parameters
|
|
----------
|
|
mouseover : bool
|
|
|
|
See Also
|
|
--------
|
|
get_cursor_data
|
|
.ToolCursorPosition
|
|
.NavigationToolbar2
|
|
"""
|
|
self._mouseover = bool(mouseover)
|
|
ax = self.axes
|
|
if ax:
|
|
if self._mouseover:
|
|
ax._mouseover_set.add(self)
|
|
else:
|
|
ax._mouseover_set.discard(self)
|
|
|
|
mouseover = property(get_mouseover, set_mouseover) # backcompat.
|
|
|
|
|
|
def _get_tightbbox_for_layout_only(obj, *args, **kwargs):
|
|
"""
|
|
Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a
|
|
*for_layout_only* kwarg; this helper tries to use the kwarg but skips it
|
|
when encountering third-party subclasses that do not support it.
|
|
"""
|
|
try:
|
|
return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True})
|
|
except TypeError:
|
|
return obj.get_tightbbox(*args, **kwargs)
|
|
|
|
|
|
class ArtistInspector:
|
|
"""
|
|
A helper class to inspect an `~matplotlib.artist.Artist` and return
|
|
information about its settable properties and their current values.
|
|
"""
|
|
|
|
def __init__(self, o):
|
|
r"""
|
|
Initialize the artist inspector with an `Artist` or an iterable of
|
|
`Artist`\s. If an iterable is used, we assume it is a homogeneous
|
|
sequence (all `Artist`\s are of the same type) and it is your
|
|
responsibility to make sure this is so.
|
|
"""
|
|
if not isinstance(o, Artist):
|
|
if np.iterable(o):
|
|
o = list(o)
|
|
if len(o):
|
|
o = o[0]
|
|
|
|
self.oorig = o
|
|
if not isinstance(o, type):
|
|
o = type(o)
|
|
self.o = o
|
|
|
|
self.aliasd = self.get_aliases()
|
|
|
|
def get_aliases(self):
|
|
"""
|
|
Get a dict mapping property fullnames to sets of aliases for each alias
|
|
in the :class:`~matplotlib.artist.ArtistInspector`.
|
|
|
|
e.g., for lines::
|
|
|
|
{'markerfacecolor': {'mfc'},
|
|
'linewidth' : {'lw'},
|
|
}
|
|
"""
|
|
names = [name for name in dir(self.o)
|
|
if name.startswith(('set_', 'get_'))
|
|
and callable(getattr(self.o, name))]
|
|
aliases = {}
|
|
for name in names:
|
|
func = getattr(self.o, name)
|
|
if not self.is_alias(func):
|
|
continue
|
|
propname = re.search(f"`({name[:4]}.*)`", # get_.*/set_.*
|
|
inspect.getdoc(func)).group(1)
|
|
aliases.setdefault(propname[4:], set()).add(name[4:])
|
|
return aliases
|
|
|
|
_get_valid_values_regex = re.compile(
|
|
r"\n\s*(?:\.\.\s+)?ACCEPTS:\s*((?:.|\n)*?)(?:$|(?:\n\n))"
|
|
)
|
|
|
|
def get_valid_values(self, attr):
|
|
"""
|
|
Get the legal arguments for the setter associated with *attr*.
|
|
|
|
This is done by querying the docstring of the setter for a line that
|
|
begins with "ACCEPTS:" or ".. ACCEPTS:", and then by looking for a
|
|
numpydoc-style documentation for the setter's first argument.
|
|
"""
|
|
|
|
name = 'set_%s' % attr
|
|
if not hasattr(self.o, name):
|
|
raise AttributeError(f'{self.o} has no function {name}')
|
|
func = getattr(self.o, name)
|
|
|
|
if hasattr(func, '_kwarg_doc'):
|
|
return func._kwarg_doc
|
|
|
|
docstring = inspect.getdoc(func)
|
|
if docstring is None:
|
|
return 'unknown'
|
|
|
|
if docstring.startswith('Alias for '):
|
|
return None
|
|
|
|
match = self._get_valid_values_regex.search(docstring)
|
|
if match is not None:
|
|
return re.sub("\n *", " ", match.group(1))
|
|
|
|
# Much faster than list(inspect.signature(func).parameters)[1],
|
|
# although barely relevant wrt. matplotlib's total import time.
|
|
param_name = func.__code__.co_varnames[1]
|
|
# We could set the presence * based on whether the parameter is a
|
|
# varargs (it can't be a varkwargs) but it's not really worth it.
|
|
match = re.search(fr"(?m)^ *\*?{param_name} : (.+)", docstring)
|
|
if match:
|
|
return match.group(1)
|
|
|
|
return 'unknown'
|
|
|
|
def _replace_path(self, source_class):
|
|
"""
|
|
Changes the full path to the public API path that is used
|
|
in sphinx. This is needed for links to work.
|
|
"""
|
|
replace_dict = {'_base._AxesBase': 'Axes',
|
|
'_axes.Axes': 'Axes'}
|
|
for key, value in replace_dict.items():
|
|
source_class = source_class.replace(key, value)
|
|
return source_class
|
|
|
|
def get_setters(self):
|
|
"""
|
|
Get the attribute strings with setters for object.
|
|
|
|
For example, for a line, return ``['markerfacecolor', 'linewidth',
|
|
....]``.
|
|
"""
|
|
setters = []
|
|
for name in dir(self.o):
|
|
if not name.startswith('set_'):
|
|
continue
|
|
func = getattr(self.o, name)
|
|
if (not callable(func)
|
|
or self.number_of_parameters(func) < 2
|
|
or self.is_alias(func)):
|
|
continue
|
|
setters.append(name[4:])
|
|
return setters
|
|
|
|
@staticmethod
|
|
@cache
|
|
def number_of_parameters(func):
|
|
"""Return number of parameters of the callable *func*."""
|
|
return len(inspect.signature(func).parameters)
|
|
|
|
@staticmethod
|
|
@cache
|
|
def is_alias(method):
|
|
"""
|
|
Return whether the object *method* is an alias for another method.
|
|
"""
|
|
|
|
ds = inspect.getdoc(method)
|
|
if ds is None:
|
|
return False
|
|
|
|
return ds.startswith('Alias for ')
|
|
|
|
def aliased_name(self, s):
|
|
"""
|
|
Return 'PROPNAME or alias' if *s* has an alias, else return 'PROPNAME'.
|
|
|
|
For example, for the line markerfacecolor property, which has an
|
|
alias, return 'markerfacecolor or mfc' and for the transform
|
|
property, which does not, return 'transform'.
|
|
"""
|
|
aliases = ''.join(' or %s' % x for x in sorted(self.aliasd.get(s, [])))
|
|
return s + aliases
|
|
|
|
_NOT_LINKABLE = {
|
|
# A set of property setter methods that are not available in our
|
|
# current docs. This is a workaround used to prevent trying to link
|
|
# these setters which would lead to "target reference not found"
|
|
# warnings during doc build.
|
|
'matplotlib.image._ImageBase.set_alpha',
|
|
'matplotlib.image._ImageBase.set_array',
|
|
'matplotlib.image._ImageBase.set_data',
|
|
'matplotlib.image._ImageBase.set_filternorm',
|
|
'matplotlib.image._ImageBase.set_filterrad',
|
|
'matplotlib.image._ImageBase.set_interpolation',
|
|
'matplotlib.image._ImageBase.set_interpolation_stage',
|
|
'matplotlib.image._ImageBase.set_resample',
|
|
'matplotlib.text._AnnotationBase.set_annotation_clip',
|
|
}
|
|
|
|
def aliased_name_rest(self, s, target):
|
|
"""
|
|
Return 'PROPNAME or alias' if *s* has an alias, else return 'PROPNAME',
|
|
formatted for reST.
|
|
|
|
For example, for the line markerfacecolor property, which has an
|
|
alias, return 'markerfacecolor or mfc' and for the transform
|
|
property, which does not, return 'transform'.
|
|
"""
|
|
# workaround to prevent "reference target not found"
|
|
if target in self._NOT_LINKABLE:
|
|
return f'``{s}``'
|
|
|
|
aliases = ''.join(' or %s' % x for x in sorted(self.aliasd.get(s, [])))
|
|
return f':meth:`{s} <{target}>`{aliases}'
|
|
|
|
def pprint_setters(self, prop=None, leadingspace=2):
|
|
"""
|
|
If *prop* is *None*, return a list of strings of all settable
|
|
properties and their valid values.
|
|
|
|
If *prop* is not *None*, it is a valid property name and that
|
|
property will be returned as a string of property : valid
|
|
values.
|
|
"""
|
|
if leadingspace:
|
|
pad = ' ' * leadingspace
|
|
else:
|
|
pad = ''
|
|
if prop is not None:
|
|
accepts = self.get_valid_values(prop)
|
|
return f'{pad}{prop}: {accepts}'
|
|
|
|
lines = []
|
|
for prop in sorted(self.get_setters()):
|
|
accepts = self.get_valid_values(prop)
|
|
name = self.aliased_name(prop)
|
|
lines.append(f'{pad}{name}: {accepts}')
|
|
return lines
|
|
|
|
def pprint_setters_rest(self, prop=None, leadingspace=4):
|
|
"""
|
|
If *prop* is *None*, return a list of reST-formatted strings of all
|
|
settable properties and their valid values.
|
|
|
|
If *prop* is not *None*, it is a valid property name and that
|
|
property will be returned as a string of "property : valid"
|
|
values.
|
|
"""
|
|
if leadingspace:
|
|
pad = ' ' * leadingspace
|
|
else:
|
|
pad = ''
|
|
if prop is not None:
|
|
accepts = self.get_valid_values(prop)
|
|
return f'{pad}{prop}: {accepts}'
|
|
|
|
prop_and_qualnames = []
|
|
for prop in sorted(self.get_setters()):
|
|
# Find the parent method which actually provides the docstring.
|
|
for cls in self.o.__mro__:
|
|
method = getattr(cls, f"set_{prop}", None)
|
|
if method and method.__doc__ is not None:
|
|
break
|
|
else: # No docstring available.
|
|
method = getattr(self.o, f"set_{prop}")
|
|
prop_and_qualnames.append(
|
|
(prop, f"{method.__module__}.{method.__qualname__}"))
|
|
|
|
names = [self.aliased_name_rest(prop, target)
|
|
.replace('_base._AxesBase', 'Axes')
|
|
.replace('_axes.Axes', 'Axes')
|
|
for prop, target in prop_and_qualnames]
|
|
accepts = [self.get_valid_values(prop)
|
|
for prop, _ in prop_and_qualnames]
|
|
|
|
col0_len = max(len(n) for n in names)
|
|
col1_len = max(len(a) for a in accepts)
|
|
table_formatstr = pad + ' ' + '=' * col0_len + ' ' + '=' * col1_len
|
|
|
|
return [
|
|
'',
|
|
pad + '.. table::',
|
|
pad + ' :class: property-table',
|
|
'',
|
|
table_formatstr,
|
|
pad + ' ' + 'Property'.ljust(col0_len)
|
|
+ ' ' + 'Description'.ljust(col1_len),
|
|
table_formatstr,
|
|
*[pad + ' ' + n.ljust(col0_len) + ' ' + a.ljust(col1_len)
|
|
for n, a in zip(names, accepts)],
|
|
table_formatstr,
|
|
'',
|
|
]
|
|
|
|
def properties(self):
|
|
"""Return a dictionary mapping property name -> value."""
|
|
o = self.oorig
|
|
getters = [name for name in dir(o)
|
|
if name.startswith('get_') and callable(getattr(o, name))]
|
|
getters.sort()
|
|
d = {}
|
|
for name in getters:
|
|
func = getattr(o, name)
|
|
if self.is_alias(func):
|
|
continue
|
|
try:
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter('ignore')
|
|
val = func()
|
|
except Exception:
|
|
continue
|
|
else:
|
|
d[name[4:]] = val
|
|
return d
|
|
|
|
def pprint_getters(self):
|
|
"""Return the getters and actual values as list of strings."""
|
|
lines = []
|
|
for name, val in sorted(self.properties().items()):
|
|
if getattr(val, 'shape', ()) != () and len(val) > 6:
|
|
s = str(val[:6]) + '...'
|
|
else:
|
|
s = str(val)
|
|
s = s.replace('\n', ' ')
|
|
if len(s) > 50:
|
|
s = s[:50] + '...'
|
|
name = self.aliased_name(name)
|
|
lines.append(f' {name} = {s}')
|
|
return lines
|
|
|
|
|
|
def getp(obj, property=None):
|
|
"""
|
|
Return the value of an `.Artist`'s *property*, or print all of them.
|
|
|
|
Parameters
|
|
----------
|
|
obj : `~matplotlib.artist.Artist`
|
|
The queried artist; e.g., a `.Line2D`, a `.Text`, or an `~.axes.Axes`.
|
|
|
|
property : str or None, default: None
|
|
If *property* is 'somename', this function returns
|
|
``obj.get_somename()``.
|
|
|
|
If it's None (or unset), it *prints* all gettable properties from
|
|
*obj*. Many properties have aliases for shorter typing, e.g. 'lw' is
|
|
an alias for 'linewidth'. In the output, aliases and full property
|
|
names will be listed as:
|
|
|
|
property or alias = value
|
|
|
|
e.g.:
|
|
|
|
linewidth or lw = 2
|
|
|
|
See Also
|
|
--------
|
|
setp
|
|
"""
|
|
if property is None:
|
|
insp = ArtistInspector(obj)
|
|
ret = insp.pprint_getters()
|
|
print('\n'.join(ret))
|
|
return
|
|
return getattr(obj, 'get_' + property)()
|
|
|
|
# alias
|
|
get = getp
|
|
|
|
|
|
def setp(obj, *args, file=None, **kwargs):
|
|
"""
|
|
Set one or more properties on an `.Artist`, or list allowed values.
|
|
|
|
Parameters
|
|
----------
|
|
obj : `~matplotlib.artist.Artist` or list of `.Artist`
|
|
The artist(s) whose properties are being set or queried. When setting
|
|
properties, all artists are affected; when querying the allowed values,
|
|
only the first instance in the sequence is queried.
|
|
|
|
For example, two lines can be made thicker and red with a single call:
|
|
|
|
>>> x = arange(0, 1, 0.01)
|
|
>>> lines = plot(x, sin(2*pi*x), x, sin(4*pi*x))
|
|
>>> setp(lines, linewidth=2, color='r')
|
|
|
|
file : file-like, default: `sys.stdout`
|
|
Where `setp` writes its output when asked to list allowed values.
|
|
|
|
>>> with open('output.log') as file:
|
|
... setp(line, file=file)
|
|
|
|
The default, ``None``, means `sys.stdout`.
|
|
|
|
*args, **kwargs
|
|
The properties to set. The following combinations are supported:
|
|
|
|
- Set the linestyle of a line to be dashed:
|
|
|
|
>>> line, = plot([1, 2, 3])
|
|
>>> setp(line, linestyle='--')
|
|
|
|
- Set multiple properties at once:
|
|
|
|
>>> setp(line, linewidth=2, color='r')
|
|
|
|
- List allowed values for a line's linestyle:
|
|
|
|
>>> setp(line, 'linestyle')
|
|
linestyle: {'-', '--', '-.', ':', '', (offset, on-off-seq), ...}
|
|
|
|
- List all properties that can be set, and their allowed values:
|
|
|
|
>>> setp(line)
|
|
agg_filter: a filter function, ...
|
|
[long output listing omitted]
|
|
|
|
`setp` also supports MATLAB style string/value pairs. For example, the
|
|
following are equivalent:
|
|
|
|
>>> setp(lines, 'linewidth', 2, 'color', 'r') # MATLAB style
|
|
>>> setp(lines, linewidth=2, color='r') # Python style
|
|
|
|
See Also
|
|
--------
|
|
getp
|
|
"""
|
|
|
|
if isinstance(obj, Artist):
|
|
objs = [obj]
|
|
else:
|
|
objs = list(cbook.flatten(obj))
|
|
|
|
if not objs:
|
|
return
|
|
|
|
insp = ArtistInspector(objs[0])
|
|
|
|
if not kwargs and len(args) < 2:
|
|
if args:
|
|
print(insp.pprint_setters(prop=args[0]), file=file)
|
|
else:
|
|
print('\n'.join(insp.pprint_setters()), file=file)
|
|
return
|
|
|
|
if len(args) % 2:
|
|
raise ValueError('The set args must be string, value pairs')
|
|
|
|
funcvals = dict(zip(args[::2], args[1::2]))
|
|
ret = [o.update(funcvals) for o in objs] + [o.set(**kwargs) for o in objs]
|
|
return list(cbook.flatten(ret))
|
|
|
|
|
|
def kwdoc(artist):
|
|
r"""
|
|
Inspect an `~matplotlib.artist.Artist` class (using `.ArtistInspector`) and
|
|
return information about its settable properties and their current values.
|
|
|
|
Parameters
|
|
----------
|
|
artist : `~matplotlib.artist.Artist` or an iterable of `Artist`\s
|
|
|
|
Returns
|
|
-------
|
|
str
|
|
The settable properties of *artist*, as plain text if
|
|
:rc:`docstring.hardcopy` is False and as a rst table (intended for
|
|
use in Sphinx) if it is True.
|
|
"""
|
|
ai = ArtistInspector(artist)
|
|
return ('\n'.join(ai.pprint_setters_rest(leadingspace=4))
|
|
if mpl.rcParams['docstring.hardcopy'] else
|
|
'Properties:\n' + '\n'.join(ai.pprint_setters(leadingspace=4)))
|
|
|
|
# We defer this to the end of them module, because it needs ArtistInspector
|
|
# to be defined.
|
|
Artist._update_set_signature_and_docstring()
|