1679 lines
55 KiB
Python
1679 lines
55 KiB
Python
"""
|
|
2D lines with support for a variety of line styles, markers, colors, etc.
|
|
"""
|
|
|
|
import copy
|
|
|
|
from numbers import Integral, Number, Real
|
|
import logging
|
|
|
|
import numpy as np
|
|
|
|
import matplotlib as mpl
|
|
from . import _api, cbook, colors as mcolors, _docstring
|
|
from .artist import Artist, allow_rasterization
|
|
from .cbook import (
|
|
_to_unmasked_float_array, ls_mapper, ls_mapper_r, STEP_LOOKUP_MAP)
|
|
from .markers import MarkerStyle
|
|
from .path import Path
|
|
from .transforms import Bbox, BboxTransformTo, TransformedPath
|
|
from ._enums import JoinStyle, CapStyle
|
|
|
|
# Imported here for backward compatibility, even though they don't
|
|
# really belong.
|
|
from . import _path
|
|
from .markers import ( # noqa
|
|
CARETLEFT, CARETRIGHT, CARETUP, CARETDOWN,
|
|
CARETLEFTBASE, CARETRIGHTBASE, CARETUPBASE, CARETDOWNBASE,
|
|
TICKLEFT, TICKRIGHT, TICKUP, TICKDOWN)
|
|
|
|
_log = logging.getLogger(__name__)
|
|
|
|
|
|
def _get_dash_pattern(style):
|
|
"""Convert linestyle to dash pattern."""
|
|
# go from short hand -> full strings
|
|
if isinstance(style, str):
|
|
style = ls_mapper.get(style, style)
|
|
# un-dashed styles
|
|
if style in ['solid', 'None']:
|
|
offset = 0
|
|
dashes = None
|
|
# dashed styles
|
|
elif style in ['dashed', 'dashdot', 'dotted']:
|
|
offset = 0
|
|
dashes = tuple(mpl.rcParams[f'lines.{style}_pattern'])
|
|
#
|
|
elif isinstance(style, tuple):
|
|
offset, dashes = style
|
|
if offset is None:
|
|
raise ValueError(f'Unrecognized linestyle: {style!r}')
|
|
else:
|
|
raise ValueError(f'Unrecognized linestyle: {style!r}')
|
|
|
|
# normalize offset to be positive and shorter than the dash cycle
|
|
if dashes is not None:
|
|
dsum = sum(dashes)
|
|
if dsum:
|
|
offset %= dsum
|
|
|
|
return offset, dashes
|
|
|
|
|
|
def _get_inverse_dash_pattern(offset, dashes):
|
|
"""Return the inverse of the given dash pattern, for filling the gaps."""
|
|
# Define the inverse pattern by moving the last gap to the start of the
|
|
# sequence.
|
|
gaps = dashes[-1:] + dashes[:-1]
|
|
# Set the offset so that this new first segment is skipped
|
|
# (see backend_bases.GraphicsContextBase.set_dashes for offset definition).
|
|
offset_gaps = offset + dashes[-1]
|
|
|
|
return offset_gaps, gaps
|
|
|
|
|
|
def _scale_dashes(offset, dashes, lw):
|
|
if not mpl.rcParams['lines.scale_dashes']:
|
|
return offset, dashes
|
|
scaled_offset = offset * lw
|
|
scaled_dashes = ([x * lw if x is not None else None for x in dashes]
|
|
if dashes is not None else None)
|
|
return scaled_offset, scaled_dashes
|
|
|
|
|
|
def segment_hits(cx, cy, x, y, radius):
|
|
"""
|
|
Return the indices of the segments in the polyline with coordinates (*cx*,
|
|
*cy*) that are within a distance *radius* of the point (*x*, *y*).
|
|
"""
|
|
# Process single points specially
|
|
if len(x) <= 1:
|
|
res, = np.nonzero((cx - x) ** 2 + (cy - y) ** 2 <= radius ** 2)
|
|
return res
|
|
|
|
# We need to lop the last element off a lot.
|
|
xr, yr = x[:-1], y[:-1]
|
|
|
|
# Only look at line segments whose nearest point to C on the line
|
|
# lies within the segment.
|
|
dx, dy = x[1:] - xr, y[1:] - yr
|
|
Lnorm_sq = dx ** 2 + dy ** 2 # Possibly want to eliminate Lnorm==0
|
|
u = ((cx - xr) * dx + (cy - yr) * dy) / Lnorm_sq
|
|
candidates = (u >= 0) & (u <= 1)
|
|
|
|
# Note that there is a little area near one side of each point
|
|
# which will be near neither segment, and another which will
|
|
# be near both, depending on the angle of the lines. The
|
|
# following radius test eliminates these ambiguities.
|
|
point_hits = (cx - x) ** 2 + (cy - y) ** 2 <= radius ** 2
|
|
candidates = candidates & ~(point_hits[:-1] | point_hits[1:])
|
|
|
|
# For those candidates which remain, determine how far they lie away
|
|
# from the line.
|
|
px, py = xr + u * dx, yr + u * dy
|
|
line_hits = (cx - px) ** 2 + (cy - py) ** 2 <= radius ** 2
|
|
line_hits = line_hits & candidates
|
|
points, = point_hits.ravel().nonzero()
|
|
lines, = line_hits.ravel().nonzero()
|
|
return np.concatenate((points, lines))
|
|
|
|
|
|
def _mark_every_path(markevery, tpath, affine, ax):
|
|
"""
|
|
Helper function that sorts out how to deal the input
|
|
`markevery` and returns the points where markers should be drawn.
|
|
|
|
Takes in the `markevery` value and the line path and returns the
|
|
sub-sampled path.
|
|
"""
|
|
# pull out the two bits of data we want from the path
|
|
codes, verts = tpath.codes, tpath.vertices
|
|
|
|
def _slice_or_none(in_v, slc):
|
|
"""Helper function to cope with `codes` being an ndarray or `None`."""
|
|
if in_v is None:
|
|
return None
|
|
return in_v[slc]
|
|
|
|
# if just an int, assume starting at 0 and make a tuple
|
|
if isinstance(markevery, Integral):
|
|
markevery = (0, markevery)
|
|
# if just a float, assume starting at 0.0 and make a tuple
|
|
elif isinstance(markevery, Real):
|
|
markevery = (0.0, markevery)
|
|
|
|
if isinstance(markevery, tuple):
|
|
if len(markevery) != 2:
|
|
raise ValueError('`markevery` is a tuple but its len is not 2; '
|
|
f'markevery={markevery}')
|
|
start, step = markevery
|
|
# if step is an int, old behavior
|
|
if isinstance(step, Integral):
|
|
# tuple of 2 int is for backwards compatibility,
|
|
if not isinstance(start, Integral):
|
|
raise ValueError(
|
|
'`markevery` is a tuple with len 2 and second element is '
|
|
'an int, but the first element is not an int; '
|
|
f'markevery={markevery}')
|
|
# just return, we are done here
|
|
|
|
return Path(verts[slice(start, None, step)],
|
|
_slice_or_none(codes, slice(start, None, step)))
|
|
|
|
elif isinstance(step, Real):
|
|
if not isinstance(start, Real):
|
|
raise ValueError(
|
|
'`markevery` is a tuple with len 2 and second element is '
|
|
'a float, but the first element is not a float or an int; '
|
|
f'markevery={markevery}')
|
|
if ax is None:
|
|
raise ValueError(
|
|
"markevery is specified relative to the axes size, but "
|
|
"the line does not have a Axes as parent")
|
|
|
|
# calc cumulative distance along path (in display coords):
|
|
fin = np.isfinite(verts).all(axis=1)
|
|
fverts = verts[fin]
|
|
disp_coords = affine.transform(fverts)
|
|
|
|
delta = np.empty((len(disp_coords), 2))
|
|
delta[0, :] = 0
|
|
delta[1:, :] = disp_coords[1:, :] - disp_coords[:-1, :]
|
|
delta = np.hypot(*delta.T).cumsum()
|
|
# calc distance between markers along path based on the axes
|
|
# bounding box diagonal being a distance of unity:
|
|
(x0, y0), (x1, y1) = ax.transAxes.transform([[0, 0], [1, 1]])
|
|
scale = np.hypot(x1 - x0, y1 - y0)
|
|
marker_delta = np.arange(start * scale, delta[-1], step * scale)
|
|
# find closest actual data point that is closest to
|
|
# the theoretical distance along the path:
|
|
inds = np.abs(delta[np.newaxis, :] - marker_delta[:, np.newaxis])
|
|
inds = inds.argmin(axis=1)
|
|
inds = np.unique(inds)
|
|
# return, we are done here
|
|
return Path(fverts[inds], _slice_or_none(codes, inds))
|
|
else:
|
|
raise ValueError(
|
|
f"markevery={markevery!r} is a tuple with len 2, but its "
|
|
f"second element is not an int or a float")
|
|
|
|
elif isinstance(markevery, slice):
|
|
# mazol tov, it's already a slice, just return
|
|
return Path(verts[markevery], _slice_or_none(codes, markevery))
|
|
|
|
elif np.iterable(markevery):
|
|
# fancy indexing
|
|
try:
|
|
return Path(verts[markevery], _slice_or_none(codes, markevery))
|
|
except (ValueError, IndexError) as err:
|
|
raise ValueError(
|
|
f"markevery={markevery!r} is iterable but not a valid numpy "
|
|
f"fancy index") from err
|
|
else:
|
|
raise ValueError(f"markevery={markevery!r} is not a recognized value")
|
|
|
|
|
|
@_docstring.interpd
|
|
@_api.define_aliases({
|
|
"antialiased": ["aa"],
|
|
"color": ["c"],
|
|
"drawstyle": ["ds"],
|
|
"linestyle": ["ls"],
|
|
"linewidth": ["lw"],
|
|
"markeredgecolor": ["mec"],
|
|
"markeredgewidth": ["mew"],
|
|
"markerfacecolor": ["mfc"],
|
|
"markerfacecoloralt": ["mfcalt"],
|
|
"markersize": ["ms"],
|
|
})
|
|
class Line2D(Artist):
|
|
"""
|
|
A line - the line can have both a solid linestyle connecting all
|
|
the vertices, and a marker at each vertex. Additionally, the
|
|
drawing of the solid line is influenced by the drawstyle, e.g., one
|
|
can create "stepped" lines in various styles.
|
|
"""
|
|
|
|
lineStyles = _lineStyles = { # hidden names deprecated
|
|
'-': '_draw_solid',
|
|
'--': '_draw_dashed',
|
|
'-.': '_draw_dash_dot',
|
|
':': '_draw_dotted',
|
|
'None': '_draw_nothing',
|
|
' ': '_draw_nothing',
|
|
'': '_draw_nothing',
|
|
}
|
|
|
|
_drawStyles_l = {
|
|
'default': '_draw_lines',
|
|
'steps-mid': '_draw_steps_mid',
|
|
'steps-pre': '_draw_steps_pre',
|
|
'steps-post': '_draw_steps_post',
|
|
}
|
|
|
|
_drawStyles_s = {
|
|
'steps': '_draw_steps_pre',
|
|
}
|
|
|
|
# drawStyles should now be deprecated.
|
|
drawStyles = {**_drawStyles_l, **_drawStyles_s}
|
|
# Need a list ordered with long names first:
|
|
drawStyleKeys = [*_drawStyles_l, *_drawStyles_s]
|
|
|
|
# Referenced here to maintain API. These are defined in
|
|
# MarkerStyle
|
|
markers = MarkerStyle.markers
|
|
filled_markers = MarkerStyle.filled_markers
|
|
fillStyles = MarkerStyle.fillstyles
|
|
|
|
zorder = 2
|
|
|
|
_subslice_optim_min_size = 1000
|
|
|
|
def __str__(self):
|
|
if self._label != "":
|
|
return f"Line2D({self._label})"
|
|
elif self._x is None:
|
|
return "Line2D()"
|
|
elif len(self._x) > 3:
|
|
return "Line2D(({:g},{:g}),({:g},{:g}),...,({:g},{:g}))".format(
|
|
self._x[0], self._y[0],
|
|
self._x[1], self._y[1],
|
|
self._x[-1], self._y[-1])
|
|
else:
|
|
return "Line2D(%s)" % ",".join(
|
|
map("({:g},{:g})".format, self._x, self._y))
|
|
|
|
def __init__(self, xdata, ydata, *,
|
|
linewidth=None, # all Nones default to rc
|
|
linestyle=None,
|
|
color=None,
|
|
gapcolor=None,
|
|
marker=None,
|
|
markersize=None,
|
|
markeredgewidth=None,
|
|
markeredgecolor=None,
|
|
markerfacecolor=None,
|
|
markerfacecoloralt='none',
|
|
fillstyle=None,
|
|
antialiased=None,
|
|
dash_capstyle=None,
|
|
solid_capstyle=None,
|
|
dash_joinstyle=None,
|
|
solid_joinstyle=None,
|
|
pickradius=5,
|
|
drawstyle=None,
|
|
markevery=None,
|
|
**kwargs
|
|
):
|
|
"""
|
|
Create a `.Line2D` instance with *x* and *y* data in sequences of
|
|
*xdata*, *ydata*.
|
|
|
|
Additional keyword arguments are `.Line2D` properties:
|
|
|
|
%(Line2D:kwdoc)s
|
|
|
|
See :meth:`set_linestyle` for a description of the line styles,
|
|
:meth:`set_marker` for a description of the markers, and
|
|
:meth:`set_drawstyle` for a description of the draw styles.
|
|
|
|
"""
|
|
super().__init__()
|
|
|
|
# Convert sequences to NumPy arrays.
|
|
if not np.iterable(xdata):
|
|
raise RuntimeError('xdata must be a sequence')
|
|
if not np.iterable(ydata):
|
|
raise RuntimeError('ydata must be a sequence')
|
|
|
|
if linewidth is None:
|
|
linewidth = mpl.rcParams['lines.linewidth']
|
|
|
|
if linestyle is None:
|
|
linestyle = mpl.rcParams['lines.linestyle']
|
|
if marker is None:
|
|
marker = mpl.rcParams['lines.marker']
|
|
if color is None:
|
|
color = mpl.rcParams['lines.color']
|
|
|
|
if markersize is None:
|
|
markersize = mpl.rcParams['lines.markersize']
|
|
if antialiased is None:
|
|
antialiased = mpl.rcParams['lines.antialiased']
|
|
if dash_capstyle is None:
|
|
dash_capstyle = mpl.rcParams['lines.dash_capstyle']
|
|
if dash_joinstyle is None:
|
|
dash_joinstyle = mpl.rcParams['lines.dash_joinstyle']
|
|
if solid_capstyle is None:
|
|
solid_capstyle = mpl.rcParams['lines.solid_capstyle']
|
|
if solid_joinstyle is None:
|
|
solid_joinstyle = mpl.rcParams['lines.solid_joinstyle']
|
|
|
|
if drawstyle is None:
|
|
drawstyle = 'default'
|
|
|
|
self._dashcapstyle = None
|
|
self._dashjoinstyle = None
|
|
self._solidjoinstyle = None
|
|
self._solidcapstyle = None
|
|
self.set_dash_capstyle(dash_capstyle)
|
|
self.set_dash_joinstyle(dash_joinstyle)
|
|
self.set_solid_capstyle(solid_capstyle)
|
|
self.set_solid_joinstyle(solid_joinstyle)
|
|
|
|
self._linestyles = None
|
|
self._drawstyle = None
|
|
self._linewidth = linewidth
|
|
self._unscaled_dash_pattern = (0, None) # offset, dash
|
|
self._dash_pattern = (0, None) # offset, dash (scaled by linewidth)
|
|
|
|
self.set_linewidth(linewidth)
|
|
self.set_linestyle(linestyle)
|
|
self.set_drawstyle(drawstyle)
|
|
|
|
self._color = None
|
|
self.set_color(color)
|
|
if marker is None:
|
|
marker = 'none' # Default.
|
|
if not isinstance(marker, MarkerStyle):
|
|
self._marker = MarkerStyle(marker, fillstyle)
|
|
else:
|
|
self._marker = marker
|
|
|
|
self._gapcolor = None
|
|
self.set_gapcolor(gapcolor)
|
|
|
|
self._markevery = None
|
|
self._markersize = None
|
|
self._antialiased = None
|
|
|
|
self.set_markevery(markevery)
|
|
self.set_antialiased(antialiased)
|
|
self.set_markersize(markersize)
|
|
|
|
self._markeredgecolor = None
|
|
self._markeredgewidth = None
|
|
self._markerfacecolor = None
|
|
self._markerfacecoloralt = None
|
|
|
|
self.set_markerfacecolor(markerfacecolor) # Normalizes None to rc.
|
|
self.set_markerfacecoloralt(markerfacecoloralt)
|
|
self.set_markeredgecolor(markeredgecolor) # Normalizes None to rc.
|
|
self.set_markeredgewidth(markeredgewidth)
|
|
|
|
# update kwargs before updating data to give the caller a
|
|
# chance to init axes (and hence unit support)
|
|
self._internal_update(kwargs)
|
|
self.pickradius = pickradius
|
|
self.ind_offset = 0
|
|
if (isinstance(self._picker, Number) and
|
|
not isinstance(self._picker, bool)):
|
|
self._pickradius = self._picker
|
|
|
|
self._xorig = np.asarray([])
|
|
self._yorig = np.asarray([])
|
|
self._invalidx = True
|
|
self._invalidy = True
|
|
self._x = None
|
|
self._y = None
|
|
self._xy = None
|
|
self._path = None
|
|
self._transformed_path = None
|
|
self._subslice = False
|
|
self._x_filled = None # used in subslicing; only x is needed
|
|
|
|
self.set_data(xdata, ydata)
|
|
|
|
def contains(self, mouseevent):
|
|
"""
|
|
Test whether *mouseevent* occurred on the line.
|
|
|
|
An event is deemed to have occurred "on" the line if it is less
|
|
than ``self.pickradius`` (default: 5 points) away from it. Use
|
|
`~.Line2D.get_pickradius` or `~.Line2D.set_pickradius` to get or set
|
|
the pick radius.
|
|
|
|
Parameters
|
|
----------
|
|
mouseevent : `~matplotlib.backend_bases.MouseEvent`
|
|
|
|
Returns
|
|
-------
|
|
contains : bool
|
|
Whether any values are within the radius.
|
|
details : dict
|
|
A dictionary ``{'ind': pointlist}``, where *pointlist* is a
|
|
list of points of the line that are within the pickradius around
|
|
the event position.
|
|
|
|
TODO: sort returned indices by distance
|
|
"""
|
|
if self._different_canvas(mouseevent):
|
|
return False, {}
|
|
|
|
# Make sure we have data to plot
|
|
if self._invalidy or self._invalidx:
|
|
self.recache()
|
|
if len(self._xy) == 0:
|
|
return False, {}
|
|
|
|
# Convert points to pixels
|
|
transformed_path = self._get_transformed_path()
|
|
path, affine = transformed_path.get_transformed_path_and_affine()
|
|
path = affine.transform_path(path)
|
|
xy = path.vertices
|
|
xt = xy[:, 0]
|
|
yt = xy[:, 1]
|
|
|
|
# Convert pick radius from points to pixels
|
|
if self.figure is None:
|
|
_log.warning('no figure set when check if mouse is on line')
|
|
pixels = self._pickradius
|
|
else:
|
|
pixels = self.figure.dpi / 72. * self._pickradius
|
|
|
|
# The math involved in checking for containment (here and inside of
|
|
# segment_hits) assumes that it is OK to overflow, so temporarily set
|
|
# the error flags accordingly.
|
|
with np.errstate(all='ignore'):
|
|
# Check for collision
|
|
if self._linestyle in ['None', None]:
|
|
# If no line, return the nearby point(s)
|
|
ind, = np.nonzero(
|
|
(xt - mouseevent.x) ** 2 + (yt - mouseevent.y) ** 2
|
|
<= pixels ** 2)
|
|
else:
|
|
# If line, return the nearby segment(s)
|
|
ind = segment_hits(mouseevent.x, mouseevent.y, xt, yt, pixels)
|
|
if self._drawstyle.startswith("steps"):
|
|
ind //= 2
|
|
|
|
ind += self.ind_offset
|
|
|
|
# Return the point(s) within radius
|
|
return len(ind) > 0, dict(ind=ind)
|
|
|
|
def get_pickradius(self):
|
|
"""
|
|
Return the pick radius used for containment tests.
|
|
|
|
See `.contains` for more details.
|
|
"""
|
|
return self._pickradius
|
|
|
|
def set_pickradius(self, pickradius):
|
|
"""
|
|
Set the pick radius used for containment tests.
|
|
|
|
See `.contains` for more details.
|
|
|
|
Parameters
|
|
----------
|
|
pickradius : float
|
|
Pick radius, in points.
|
|
"""
|
|
if not isinstance(pickradius, Real) or pickradius < 0:
|
|
raise ValueError("pick radius should be a distance")
|
|
self._pickradius = pickradius
|
|
|
|
pickradius = property(get_pickradius, set_pickradius)
|
|
|
|
def get_fillstyle(self):
|
|
"""
|
|
Return the marker fill style.
|
|
|
|
See also `~.Line2D.set_fillstyle`.
|
|
"""
|
|
return self._marker.get_fillstyle()
|
|
|
|
def set_fillstyle(self, fs):
|
|
"""
|
|
Set the marker fill style.
|
|
|
|
Parameters
|
|
----------
|
|
fs : {'full', 'left', 'right', 'bottom', 'top', 'none'}
|
|
Possible values:
|
|
|
|
- 'full': Fill the whole marker with the *markerfacecolor*.
|
|
- 'left', 'right', 'bottom', 'top': Fill the marker half at
|
|
the given side with the *markerfacecolor*. The other
|
|
half of the marker is filled with *markerfacecoloralt*.
|
|
- 'none': No filling.
|
|
|
|
For examples see :ref:`marker_fill_styles`.
|
|
"""
|
|
self.set_marker(MarkerStyle(self._marker.get_marker(), fs))
|
|
self.stale = True
|
|
|
|
def set_markevery(self, every):
|
|
"""
|
|
Set the markevery property to subsample the plot when using markers.
|
|
|
|
e.g., if ``every=5``, every 5-th marker will be plotted.
|
|
|
|
Parameters
|
|
----------
|
|
every : None or int or (int, int) or slice or list[int] or float or \
|
|
(float, float) or list[bool]
|
|
Which markers to plot.
|
|
|
|
- ``every=None``: every point will be plotted.
|
|
- ``every=N``: every N-th marker will be plotted starting with
|
|
marker 0.
|
|
- ``every=(start, N)``: every N-th marker, starting at index
|
|
*start*, will be plotted.
|
|
- ``every=slice(start, end, N)``: every N-th marker, starting at
|
|
index *start*, up to but not including index *end*, will be
|
|
plotted.
|
|
- ``every=[i, j, m, ...]``: only markers at the given indices
|
|
will be plotted.
|
|
- ``every=[True, False, True, ...]``: only positions that are True
|
|
will be plotted. The list must have the same length as the data
|
|
points.
|
|
- ``every=0.1``, (i.e. a float): markers will be spaced at
|
|
approximately equal visual distances along the line; the distance
|
|
along the line between markers is determined by multiplying the
|
|
display-coordinate distance of the axes bounding-box diagonal
|
|
by the value of *every*.
|
|
- ``every=(0.5, 0.1)`` (i.e. a length-2 tuple of float): similar
|
|
to ``every=0.1`` but the first marker will be offset along the
|
|
line by 0.5 multiplied by the
|
|
display-coordinate-diagonal-distance along the line.
|
|
|
|
For examples see
|
|
:doc:`/gallery/lines_bars_and_markers/markevery_demo`.
|
|
|
|
Notes
|
|
-----
|
|
Setting *markevery* will still only draw markers at actual data points.
|
|
While the float argument form aims for uniform visual spacing, it has
|
|
to coerce from the ideal spacing to the nearest available data point.
|
|
Depending on the number and distribution of data points, the result
|
|
may still not look evenly spaced.
|
|
|
|
When using a start offset to specify the first marker, the offset will
|
|
be from the first data point which may be different from the first
|
|
the visible data point if the plot is zoomed in.
|
|
|
|
If zooming in on a plot when using float arguments then the actual
|
|
data points that have markers will change because the distance between
|
|
markers is always determined from the display-coordinates
|
|
axes-bounding-box-diagonal regardless of the actual axes data limits.
|
|
|
|
"""
|
|
self._markevery = every
|
|
self.stale = True
|
|
|
|
def get_markevery(self):
|
|
"""
|
|
Return the markevery setting for marker subsampling.
|
|
|
|
See also `~.Line2D.set_markevery`.
|
|
"""
|
|
return self._markevery
|
|
|
|
def set_picker(self, p):
|
|
"""
|
|
Set the event picker details for the line.
|
|
|
|
Parameters
|
|
----------
|
|
p : float or callable[[Artist, Event], tuple[bool, dict]]
|
|
If a float, it is used as the pick radius in points.
|
|
"""
|
|
if not callable(p):
|
|
self.set_pickradius(p)
|
|
self._picker = p
|
|
|
|
def get_bbox(self):
|
|
"""Get the bounding box of this line."""
|
|
bbox = Bbox([[0, 0], [0, 0]])
|
|
bbox.update_from_data_xy(self.get_xydata())
|
|
return bbox
|
|
|
|
def get_window_extent(self, renderer=None):
|
|
bbox = Bbox([[0, 0], [0, 0]])
|
|
trans_data_to_xy = self.get_transform().transform
|
|
bbox.update_from_data_xy(trans_data_to_xy(self.get_xydata()),
|
|
ignore=True)
|
|
# correct for marker size, if any
|
|
if self._marker:
|
|
ms = (self._markersize / 72.0 * self.figure.dpi) * 0.5
|
|
bbox = bbox.padded(ms)
|
|
return bbox
|
|
|
|
def set_data(self, *args):
|
|
"""
|
|
Set the x and y data.
|
|
|
|
Parameters
|
|
----------
|
|
*args : (2, N) array or two 1D arrays
|
|
"""
|
|
if len(args) == 1:
|
|
(x, y), = args
|
|
else:
|
|
x, y = args
|
|
|
|
self.set_xdata(x)
|
|
self.set_ydata(y)
|
|
|
|
def recache_always(self):
|
|
self.recache(always=True)
|
|
|
|
def recache(self, always=False):
|
|
if always or self._invalidx:
|
|
xconv = self.convert_xunits(self._xorig)
|
|
x = _to_unmasked_float_array(xconv).ravel()
|
|
else:
|
|
x = self._x
|
|
if always or self._invalidy:
|
|
yconv = self.convert_yunits(self._yorig)
|
|
y = _to_unmasked_float_array(yconv).ravel()
|
|
else:
|
|
y = self._y
|
|
|
|
self._xy = np.column_stack(np.broadcast_arrays(x, y)).astype(float)
|
|
self._x, self._y = self._xy.T # views
|
|
|
|
self._subslice = False
|
|
if (self.axes
|
|
and len(x) > self._subslice_optim_min_size
|
|
and _path.is_sorted_and_has_non_nan(x)
|
|
and self.axes.name == 'rectilinear'
|
|
and self.axes.get_xscale() == 'linear'
|
|
and self._markevery is None
|
|
and self.get_clip_on()
|
|
and self.get_transform() == self.axes.transData):
|
|
self._subslice = True
|
|
nanmask = np.isnan(x)
|
|
if nanmask.any():
|
|
self._x_filled = self._x.copy()
|
|
indices = np.arange(len(x))
|
|
self._x_filled[nanmask] = np.interp(
|
|
indices[nanmask], indices[~nanmask], self._x[~nanmask])
|
|
else:
|
|
self._x_filled = self._x
|
|
|
|
if self._path is not None:
|
|
interpolation_steps = self._path._interpolation_steps
|
|
else:
|
|
interpolation_steps = 1
|
|
xy = STEP_LOOKUP_MAP[self._drawstyle](*self._xy.T)
|
|
self._path = Path(np.asarray(xy).T,
|
|
_interpolation_steps=interpolation_steps)
|
|
self._transformed_path = None
|
|
self._invalidx = False
|
|
self._invalidy = False
|
|
|
|
def _transform_path(self, subslice=None):
|
|
"""
|
|
Put a TransformedPath instance at self._transformed_path;
|
|
all invalidation of the transform is then handled by the
|
|
TransformedPath instance.
|
|
"""
|
|
# Masked arrays are now handled by the Path class itself
|
|
if subslice is not None:
|
|
xy = STEP_LOOKUP_MAP[self._drawstyle](*self._xy[subslice, :].T)
|
|
_path = Path(np.asarray(xy).T,
|
|
_interpolation_steps=self._path._interpolation_steps)
|
|
else:
|
|
_path = self._path
|
|
self._transformed_path = TransformedPath(_path, self.get_transform())
|
|
|
|
def _get_transformed_path(self):
|
|
"""Return this line's `~matplotlib.transforms.TransformedPath`."""
|
|
if self._transformed_path is None:
|
|
self._transform_path()
|
|
return self._transformed_path
|
|
|
|
def set_transform(self, t):
|
|
# docstring inherited
|
|
self._invalidx = True
|
|
self._invalidy = True
|
|
super().set_transform(t)
|
|
|
|
@allow_rasterization
|
|
def draw(self, renderer):
|
|
# docstring inherited
|
|
|
|
if not self.get_visible():
|
|
return
|
|
|
|
if self._invalidy or self._invalidx:
|
|
self.recache()
|
|
self.ind_offset = 0 # Needed for contains() method.
|
|
if self._subslice and self.axes:
|
|
x0, x1 = self.axes.get_xbound()
|
|
i0 = self._x_filled.searchsorted(x0, 'left')
|
|
i1 = self._x_filled.searchsorted(x1, 'right')
|
|
subslice = slice(max(i0 - 1, 0), i1 + 1)
|
|
self.ind_offset = subslice.start
|
|
self._transform_path(subslice)
|
|
else:
|
|
subslice = None
|
|
|
|
if self.get_path_effects():
|
|
from matplotlib.patheffects import PathEffectRenderer
|
|
renderer = PathEffectRenderer(self.get_path_effects(), renderer)
|
|
|
|
renderer.open_group('line2d', self.get_gid())
|
|
if self._lineStyles[self._linestyle] != '_draw_nothing':
|
|
tpath, affine = (self._get_transformed_path()
|
|
.get_transformed_path_and_affine())
|
|
if len(tpath.vertices):
|
|
gc = renderer.new_gc()
|
|
self._set_gc_clip(gc)
|
|
gc.set_url(self.get_url())
|
|
|
|
gc.set_antialiased(self._antialiased)
|
|
gc.set_linewidth(self._linewidth)
|
|
|
|
if self.is_dashed():
|
|
cap = self._dashcapstyle
|
|
join = self._dashjoinstyle
|
|
else:
|
|
cap = self._solidcapstyle
|
|
join = self._solidjoinstyle
|
|
gc.set_joinstyle(join)
|
|
gc.set_capstyle(cap)
|
|
gc.set_snap(self.get_snap())
|
|
if self.get_sketch_params() is not None:
|
|
gc.set_sketch_params(*self.get_sketch_params())
|
|
|
|
# We first draw a path within the gaps if needed.
|
|
if self.is_dashed() and self._gapcolor is not None:
|
|
lc_rgba = mcolors.to_rgba(self._gapcolor, self._alpha)
|
|
gc.set_foreground(lc_rgba, isRGBA=True)
|
|
|
|
offset_gaps, gaps = _get_inverse_dash_pattern(
|
|
*self._dash_pattern)
|
|
|
|
gc.set_dashes(offset_gaps, gaps)
|
|
renderer.draw_path(gc, tpath, affine.frozen())
|
|
|
|
lc_rgba = mcolors.to_rgba(self._color, self._alpha)
|
|
gc.set_foreground(lc_rgba, isRGBA=True)
|
|
|
|
gc.set_dashes(*self._dash_pattern)
|
|
renderer.draw_path(gc, tpath, affine.frozen())
|
|
gc.restore()
|
|
|
|
if self._marker and self._markersize > 0:
|
|
gc = renderer.new_gc()
|
|
self._set_gc_clip(gc)
|
|
gc.set_url(self.get_url())
|
|
gc.set_linewidth(self._markeredgewidth)
|
|
gc.set_antialiased(self._antialiased)
|
|
|
|
ec_rgba = mcolors.to_rgba(
|
|
self.get_markeredgecolor(), self._alpha)
|
|
fc_rgba = mcolors.to_rgba(
|
|
self._get_markerfacecolor(), self._alpha)
|
|
fcalt_rgba = mcolors.to_rgba(
|
|
self._get_markerfacecolor(alt=True), self._alpha)
|
|
# If the edgecolor is "auto", it is set according to the *line*
|
|
# color but inherits the alpha value of the *face* color, if any.
|
|
if (cbook._str_equal(self._markeredgecolor, "auto")
|
|
and not cbook._str_lower_equal(
|
|
self.get_markerfacecolor(), "none")):
|
|
ec_rgba = ec_rgba[:3] + (fc_rgba[3],)
|
|
gc.set_foreground(ec_rgba, isRGBA=True)
|
|
if self.get_sketch_params() is not None:
|
|
scale, length, randomness = self.get_sketch_params()
|
|
gc.set_sketch_params(scale/2, length/2, 2*randomness)
|
|
|
|
marker = self._marker
|
|
|
|
# Markers *must* be drawn ignoring the drawstyle (but don't pay the
|
|
# recaching if drawstyle is already "default").
|
|
if self.get_drawstyle() != "default":
|
|
with cbook._setattr_cm(
|
|
self, _drawstyle="default", _transformed_path=None):
|
|
self.recache()
|
|
self._transform_path(subslice)
|
|
tpath, affine = (self._get_transformed_path()
|
|
.get_transformed_points_and_affine())
|
|
else:
|
|
tpath, affine = (self._get_transformed_path()
|
|
.get_transformed_points_and_affine())
|
|
|
|
if len(tpath.vertices):
|
|
# subsample the markers if markevery is not None
|
|
markevery = self.get_markevery()
|
|
if markevery is not None:
|
|
subsampled = _mark_every_path(
|
|
markevery, tpath, affine, self.axes)
|
|
else:
|
|
subsampled = tpath
|
|
|
|
snap = marker.get_snap_threshold()
|
|
if isinstance(snap, Real):
|
|
snap = renderer.points_to_pixels(self._markersize) >= snap
|
|
gc.set_snap(snap)
|
|
gc.set_joinstyle(marker.get_joinstyle())
|
|
gc.set_capstyle(marker.get_capstyle())
|
|
marker_path = marker.get_path()
|
|
marker_trans = marker.get_transform()
|
|
w = renderer.points_to_pixels(self._markersize)
|
|
|
|
if cbook._str_equal(marker.get_marker(), ","):
|
|
gc.set_linewidth(0)
|
|
else:
|
|
# Don't scale for pixels, and don't stroke them
|
|
marker_trans = marker_trans.scale(w)
|
|
renderer.draw_markers(gc, marker_path, marker_trans,
|
|
subsampled, affine.frozen(),
|
|
fc_rgba)
|
|
|
|
alt_marker_path = marker.get_alt_path()
|
|
if alt_marker_path:
|
|
alt_marker_trans = marker.get_alt_transform()
|
|
alt_marker_trans = alt_marker_trans.scale(w)
|
|
renderer.draw_markers(
|
|
gc, alt_marker_path, alt_marker_trans, subsampled,
|
|
affine.frozen(), fcalt_rgba)
|
|
|
|
gc.restore()
|
|
|
|
renderer.close_group('line2d')
|
|
self.stale = False
|
|
|
|
def get_antialiased(self):
|
|
"""Return whether antialiased rendering is used."""
|
|
return self._antialiased
|
|
|
|
def get_color(self):
|
|
"""
|
|
Return the line color.
|
|
|
|
See also `~.Line2D.set_color`.
|
|
"""
|
|
return self._color
|
|
|
|
def get_drawstyle(self):
|
|
"""
|
|
Return the drawstyle.
|
|
|
|
See also `~.Line2D.set_drawstyle`.
|
|
"""
|
|
return self._drawstyle
|
|
|
|
def get_gapcolor(self):
|
|
"""
|
|
Return the line gapcolor.
|
|
|
|
See also `~.Line2D.set_gapcolor`.
|
|
"""
|
|
return self._gapcolor
|
|
|
|
def get_linestyle(self):
|
|
"""
|
|
Return the linestyle.
|
|
|
|
See also `~.Line2D.set_linestyle`.
|
|
"""
|
|
return self._linestyle
|
|
|
|
def get_linewidth(self):
|
|
"""
|
|
Return the linewidth in points.
|
|
|
|
See also `~.Line2D.set_linewidth`.
|
|
"""
|
|
return self._linewidth
|
|
|
|
def get_marker(self):
|
|
"""
|
|
Return the line marker.
|
|
|
|
See also `~.Line2D.set_marker`.
|
|
"""
|
|
return self._marker.get_marker()
|
|
|
|
def get_markeredgecolor(self):
|
|
"""
|
|
Return the marker edge color.
|
|
|
|
See also `~.Line2D.set_markeredgecolor`.
|
|
"""
|
|
mec = self._markeredgecolor
|
|
if cbook._str_equal(mec, 'auto'):
|
|
if mpl.rcParams['_internal.classic_mode']:
|
|
if self._marker.get_marker() in ('.', ','):
|
|
return self._color
|
|
if (self._marker.is_filled()
|
|
and self._marker.get_fillstyle() != 'none'):
|
|
return 'k' # Bad hard-wired default...
|
|
return self._color
|
|
else:
|
|
return mec
|
|
|
|
def get_markeredgewidth(self):
|
|
"""
|
|
Return the marker edge width in points.
|
|
|
|
See also `~.Line2D.set_markeredgewidth`.
|
|
"""
|
|
return self._markeredgewidth
|
|
|
|
def _get_markerfacecolor(self, alt=False):
|
|
if self._marker.get_fillstyle() == 'none':
|
|
return 'none'
|
|
fc = self._markerfacecoloralt if alt else self._markerfacecolor
|
|
if cbook._str_lower_equal(fc, 'auto'):
|
|
return self._color
|
|
else:
|
|
return fc
|
|
|
|
def get_markerfacecolor(self):
|
|
"""
|
|
Return the marker face color.
|
|
|
|
See also `~.Line2D.set_markerfacecolor`.
|
|
"""
|
|
return self._get_markerfacecolor(alt=False)
|
|
|
|
def get_markerfacecoloralt(self):
|
|
"""
|
|
Return the alternate marker face color.
|
|
|
|
See also `~.Line2D.set_markerfacecoloralt`.
|
|
"""
|
|
return self._get_markerfacecolor(alt=True)
|
|
|
|
def get_markersize(self):
|
|
"""
|
|
Return the marker size in points.
|
|
|
|
See also `~.Line2D.set_markersize`.
|
|
"""
|
|
return self._markersize
|
|
|
|
def get_data(self, orig=True):
|
|
"""
|
|
Return the line data as an ``(xdata, ydata)`` pair.
|
|
|
|
If *orig* is *True*, return the original data.
|
|
"""
|
|
return self.get_xdata(orig=orig), self.get_ydata(orig=orig)
|
|
|
|
def get_xdata(self, orig=True):
|
|
"""
|
|
Return the xdata.
|
|
|
|
If *orig* is *True*, return the original data, else the
|
|
processed data.
|
|
"""
|
|
if orig:
|
|
return self._xorig
|
|
if self._invalidx:
|
|
self.recache()
|
|
return self._x
|
|
|
|
def get_ydata(self, orig=True):
|
|
"""
|
|
Return the ydata.
|
|
|
|
If *orig* is *True*, return the original data, else the
|
|
processed data.
|
|
"""
|
|
if orig:
|
|
return self._yorig
|
|
if self._invalidy:
|
|
self.recache()
|
|
return self._y
|
|
|
|
def get_path(self):
|
|
"""Return the `~matplotlib.path.Path` associated with this line."""
|
|
if self._invalidy or self._invalidx:
|
|
self.recache()
|
|
return self._path
|
|
|
|
def get_xydata(self):
|
|
"""Return the *xy* data as a (N, 2) array."""
|
|
if self._invalidy or self._invalidx:
|
|
self.recache()
|
|
return self._xy
|
|
|
|
def set_antialiased(self, b):
|
|
"""
|
|
Set whether to use antialiased rendering.
|
|
|
|
Parameters
|
|
----------
|
|
b : bool
|
|
"""
|
|
if self._antialiased != b:
|
|
self.stale = True
|
|
self._antialiased = b
|
|
|
|
def set_color(self, color):
|
|
"""
|
|
Set the color of the line.
|
|
|
|
Parameters
|
|
----------
|
|
color : color
|
|
"""
|
|
mcolors._check_color_like(color=color)
|
|
self._color = color
|
|
self.stale = True
|
|
|
|
def set_drawstyle(self, drawstyle):
|
|
"""
|
|
Set the drawstyle of the plot.
|
|
|
|
The drawstyle determines how the points are connected.
|
|
|
|
Parameters
|
|
----------
|
|
drawstyle : {'default', 'steps', 'steps-pre', 'steps-mid', \
|
|
'steps-post'}, default: 'default'
|
|
For 'default', the points are connected with straight lines.
|
|
|
|
The steps variants connect the points with step-like lines,
|
|
i.e. horizontal lines with vertical steps. They differ in the
|
|
location of the step:
|
|
|
|
- 'steps-pre': The step is at the beginning of the line segment,
|
|
i.e. the line will be at the y-value of point to the right.
|
|
- 'steps-mid': The step is halfway between the points.
|
|
- 'steps-post: The step is at the end of the line segment,
|
|
i.e. the line will be at the y-value of the point to the left.
|
|
- 'steps' is equal to 'steps-pre' and is maintained for
|
|
backward-compatibility.
|
|
|
|
For examples see :doc:`/gallery/lines_bars_and_markers/step_demo`.
|
|
"""
|
|
if drawstyle is None:
|
|
drawstyle = 'default'
|
|
_api.check_in_list(self.drawStyles, drawstyle=drawstyle)
|
|
if self._drawstyle != drawstyle:
|
|
self.stale = True
|
|
# invalidate to trigger a recache of the path
|
|
self._invalidx = True
|
|
self._drawstyle = drawstyle
|
|
|
|
def set_gapcolor(self, gapcolor):
|
|
"""
|
|
Set a color to fill the gaps in the dashed line style.
|
|
|
|
.. note::
|
|
|
|
Striped lines are created by drawing two interleaved dashed lines.
|
|
There can be overlaps between those two, which may result in
|
|
artifacts when using transparency.
|
|
|
|
This functionality is experimental and may change.
|
|
|
|
Parameters
|
|
----------
|
|
gapcolor : color or None
|
|
The color with which to fill the gaps. If None, the gaps are
|
|
unfilled.
|
|
"""
|
|
if gapcolor is not None:
|
|
mcolors._check_color_like(color=gapcolor)
|
|
self._gapcolor = gapcolor
|
|
self.stale = True
|
|
|
|
def set_linewidth(self, w):
|
|
"""
|
|
Set the line width in points.
|
|
|
|
Parameters
|
|
----------
|
|
w : float
|
|
Line width, in points.
|
|
"""
|
|
w = float(w)
|
|
if self._linewidth != w:
|
|
self.stale = True
|
|
self._linewidth = w
|
|
self._dash_pattern = _scale_dashes(*self._unscaled_dash_pattern, w)
|
|
|
|
def set_linestyle(self, ls):
|
|
"""
|
|
Set the linestyle of the line.
|
|
|
|
Parameters
|
|
----------
|
|
ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...}
|
|
Possible values:
|
|
|
|
- A string:
|
|
|
|
========================================== =================
|
|
linestyle description
|
|
========================================== =================
|
|
``'-'`` or ``'solid'`` solid line
|
|
``'--'`` or ``'dashed'`` dashed line
|
|
``'-.'`` or ``'dashdot'`` dash-dotted line
|
|
``':'`` or ``'dotted'`` dotted line
|
|
``'none'``, ``'None'``, ``' '``, or ``''`` draw nothing
|
|
========================================== =================
|
|
|
|
- Alternatively a dash tuple of the following form can be
|
|
provided::
|
|
|
|
(offset, onoffseq)
|
|
|
|
where ``onoffseq`` is an even length tuple of on and off ink
|
|
in points. See also :meth:`set_dashes`.
|
|
|
|
For examples see :doc:`/gallery/lines_bars_and_markers/linestyles`.
|
|
"""
|
|
if isinstance(ls, str):
|
|
if ls in [' ', '', 'none']:
|
|
ls = 'None'
|
|
_api.check_in_list([*self._lineStyles, *ls_mapper_r], ls=ls)
|
|
if ls not in self._lineStyles:
|
|
ls = ls_mapper_r[ls]
|
|
self._linestyle = ls
|
|
else:
|
|
self._linestyle = '--'
|
|
self._unscaled_dash_pattern = _get_dash_pattern(ls)
|
|
self._dash_pattern = _scale_dashes(
|
|
*self._unscaled_dash_pattern, self._linewidth)
|
|
self.stale = True
|
|
|
|
@_docstring.interpd
|
|
def set_marker(self, marker):
|
|
"""
|
|
Set the line marker.
|
|
|
|
Parameters
|
|
----------
|
|
marker : marker style string, `~.path.Path` or `~.markers.MarkerStyle`
|
|
See `~matplotlib.markers` for full description of possible
|
|
arguments.
|
|
"""
|
|
self._marker = MarkerStyle(marker, self._marker.get_fillstyle())
|
|
self.stale = True
|
|
|
|
def _set_markercolor(self, name, has_rcdefault, val):
|
|
if val is None:
|
|
val = mpl.rcParams[f"lines.{name}"] if has_rcdefault else "auto"
|
|
attr = f"_{name}"
|
|
current = getattr(self, attr)
|
|
if current is None:
|
|
self.stale = True
|
|
else:
|
|
neq = current != val
|
|
# Much faster than `np.any(current != val)` if no arrays are used.
|
|
if neq.any() if isinstance(neq, np.ndarray) else neq:
|
|
self.stale = True
|
|
setattr(self, attr, val)
|
|
|
|
def set_markeredgecolor(self, ec):
|
|
"""
|
|
Set the marker edge color.
|
|
|
|
Parameters
|
|
----------
|
|
ec : color
|
|
"""
|
|
self._set_markercolor("markeredgecolor", True, ec)
|
|
|
|
def set_markerfacecolor(self, fc):
|
|
"""
|
|
Set the marker face color.
|
|
|
|
Parameters
|
|
----------
|
|
fc : color
|
|
"""
|
|
self._set_markercolor("markerfacecolor", True, fc)
|
|
|
|
def set_markerfacecoloralt(self, fc):
|
|
"""
|
|
Set the alternate marker face color.
|
|
|
|
Parameters
|
|
----------
|
|
fc : color
|
|
"""
|
|
self._set_markercolor("markerfacecoloralt", False, fc)
|
|
|
|
def set_markeredgewidth(self, ew):
|
|
"""
|
|
Set the marker edge width in points.
|
|
|
|
Parameters
|
|
----------
|
|
ew : float
|
|
Marker edge width, in points.
|
|
"""
|
|
if ew is None:
|
|
ew = mpl.rcParams['lines.markeredgewidth']
|
|
if self._markeredgewidth != ew:
|
|
self.stale = True
|
|
self._markeredgewidth = ew
|
|
|
|
def set_markersize(self, sz):
|
|
"""
|
|
Set the marker size in points.
|
|
|
|
Parameters
|
|
----------
|
|
sz : float
|
|
Marker size, in points.
|
|
"""
|
|
sz = float(sz)
|
|
if self._markersize != sz:
|
|
self.stale = True
|
|
self._markersize = sz
|
|
|
|
def set_xdata(self, x):
|
|
"""
|
|
Set the data array for x.
|
|
|
|
Parameters
|
|
----------
|
|
x : 1D array
|
|
"""
|
|
if not np.iterable(x):
|
|
# When deprecation cycle is completed
|
|
# raise RuntimeError('x must be a sequence')
|
|
_api.warn_deprecated(
|
|
since="3.7",
|
|
message="Setting data with a non sequence type "
|
|
"is deprecated since %(since)s and will be "
|
|
"remove %(removal)s")
|
|
x = [x, ]
|
|
self._xorig = copy.copy(x)
|
|
self._invalidx = True
|
|
self.stale = True
|
|
|
|
def set_ydata(self, y):
|
|
"""
|
|
Set the data array for y.
|
|
|
|
Parameters
|
|
----------
|
|
y : 1D array
|
|
"""
|
|
if not np.iterable(y):
|
|
# When deprecation cycle is completed
|
|
# raise RuntimeError('y must be a sequence')
|
|
_api.warn_deprecated(
|
|
since="3.7",
|
|
message="Setting data with a non sequence type "
|
|
"is deprecated since %(since)s and will be "
|
|
"remove %(removal)s")
|
|
y = [y, ]
|
|
self._yorig = copy.copy(y)
|
|
self._invalidy = True
|
|
self.stale = True
|
|
|
|
def set_dashes(self, seq):
|
|
"""
|
|
Set the dash sequence.
|
|
|
|
The dash sequence is a sequence of floats of even length describing
|
|
the length of dashes and spaces in points.
|
|
|
|
For example, (5, 2, 1, 2) describes a sequence of 5 point and 1 point
|
|
dashes separated by 2 point spaces.
|
|
|
|
See also `~.Line2D.set_gapcolor`, which allows those spaces to be
|
|
filled with a color.
|
|
|
|
Parameters
|
|
----------
|
|
seq : sequence of floats (on/off ink in points) or (None, None)
|
|
If *seq* is empty or ``(None, None)``, the linestyle will be set
|
|
to solid.
|
|
"""
|
|
if seq == (None, None) or len(seq) == 0:
|
|
self.set_linestyle('-')
|
|
else:
|
|
self.set_linestyle((0, seq))
|
|
|
|
def update_from(self, other):
|
|
"""Copy properties from *other* to self."""
|
|
super().update_from(other)
|
|
self._linestyle = other._linestyle
|
|
self._linewidth = other._linewidth
|
|
self._color = other._color
|
|
self._gapcolor = other._gapcolor
|
|
self._markersize = other._markersize
|
|
self._markerfacecolor = other._markerfacecolor
|
|
self._markerfacecoloralt = other._markerfacecoloralt
|
|
self._markeredgecolor = other._markeredgecolor
|
|
self._markeredgewidth = other._markeredgewidth
|
|
self._unscaled_dash_pattern = other._unscaled_dash_pattern
|
|
self._dash_pattern = other._dash_pattern
|
|
self._dashcapstyle = other._dashcapstyle
|
|
self._dashjoinstyle = other._dashjoinstyle
|
|
self._solidcapstyle = other._solidcapstyle
|
|
self._solidjoinstyle = other._solidjoinstyle
|
|
|
|
self._linestyle = other._linestyle
|
|
self._marker = MarkerStyle(marker=other._marker)
|
|
self._drawstyle = other._drawstyle
|
|
|
|
@_docstring.interpd
|
|
def set_dash_joinstyle(self, s):
|
|
"""
|
|
How to join segments of the line if it `~Line2D.is_dashed`.
|
|
|
|
The default joinstyle is :rc:`lines.dash_joinstyle`.
|
|
|
|
Parameters
|
|
----------
|
|
s : `.JoinStyle` or %(JoinStyle)s
|
|
"""
|
|
js = JoinStyle(s)
|
|
if self._dashjoinstyle != js:
|
|
self.stale = True
|
|
self._dashjoinstyle = js
|
|
|
|
@_docstring.interpd
|
|
def set_solid_joinstyle(self, s):
|
|
"""
|
|
How to join segments if the line is solid (not `~Line2D.is_dashed`).
|
|
|
|
The default joinstyle is :rc:`lines.solid_joinstyle`.
|
|
|
|
Parameters
|
|
----------
|
|
s : `.JoinStyle` or %(JoinStyle)s
|
|
"""
|
|
js = JoinStyle(s)
|
|
if self._solidjoinstyle != js:
|
|
self.stale = True
|
|
self._solidjoinstyle = js
|
|
|
|
def get_dash_joinstyle(self):
|
|
"""
|
|
Return the `.JoinStyle` for dashed lines.
|
|
|
|
See also `~.Line2D.set_dash_joinstyle`.
|
|
"""
|
|
return self._dashjoinstyle.name
|
|
|
|
def get_solid_joinstyle(self):
|
|
"""
|
|
Return the `.JoinStyle` for solid lines.
|
|
|
|
See also `~.Line2D.set_solid_joinstyle`.
|
|
"""
|
|
return self._solidjoinstyle.name
|
|
|
|
@_docstring.interpd
|
|
def set_dash_capstyle(self, s):
|
|
"""
|
|
How to draw the end caps if the line is `~Line2D.is_dashed`.
|
|
|
|
The default capstyle is :rc:`lines.dash_capstyle`.
|
|
|
|
Parameters
|
|
----------
|
|
s : `.CapStyle` or %(CapStyle)s
|
|
"""
|
|
cs = CapStyle(s)
|
|
if self._dashcapstyle != cs:
|
|
self.stale = True
|
|
self._dashcapstyle = cs
|
|
|
|
@_docstring.interpd
|
|
def set_solid_capstyle(self, s):
|
|
"""
|
|
How to draw the end caps if the line is solid (not `~Line2D.is_dashed`)
|
|
|
|
The default capstyle is :rc:`lines.solid_capstyle`.
|
|
|
|
Parameters
|
|
----------
|
|
s : `.CapStyle` or %(CapStyle)s
|
|
"""
|
|
cs = CapStyle(s)
|
|
if self._solidcapstyle != cs:
|
|
self.stale = True
|
|
self._solidcapstyle = cs
|
|
|
|
def get_dash_capstyle(self):
|
|
"""
|
|
Return the `.CapStyle` for dashed lines.
|
|
|
|
See also `~.Line2D.set_dash_capstyle`.
|
|
"""
|
|
return self._dashcapstyle.name
|
|
|
|
def get_solid_capstyle(self):
|
|
"""
|
|
Return the `.CapStyle` for solid lines.
|
|
|
|
See also `~.Line2D.set_solid_capstyle`.
|
|
"""
|
|
return self._solidcapstyle.name
|
|
|
|
def is_dashed(self):
|
|
"""
|
|
Return whether line has a dashed linestyle.
|
|
|
|
A custom linestyle is assumed to be dashed, we do not inspect the
|
|
``onoffseq`` directly.
|
|
|
|
See also `~.Line2D.set_linestyle`.
|
|
"""
|
|
return self._linestyle in ('--', '-.', ':')
|
|
|
|
|
|
class AxLine(Line2D):
|
|
"""
|
|
A helper class that implements `~.Axes.axline`, by recomputing the artist
|
|
transform at draw time.
|
|
"""
|
|
|
|
def __init__(self, xy1, xy2, slope, **kwargs):
|
|
"""
|
|
Parameters
|
|
----------
|
|
xy1 : (float, float)
|
|
The first set of (x, y) coordinates for the line to pass through.
|
|
xy2 : (float, float) or None
|
|
The second set of (x, y) coordinates for the line to pass through.
|
|
Both *xy2* and *slope* must be passed, but one of them must be None.
|
|
slope : float or None
|
|
The slope of the line. Both *xy2* and *slope* must be passed, but one of
|
|
them must be None.
|
|
"""
|
|
super().__init__([0, 1], [0, 1], **kwargs)
|
|
|
|
if (xy2 is None and slope is None or
|
|
xy2 is not None and slope is not None):
|
|
raise TypeError(
|
|
"Exactly one of 'xy2' and 'slope' must be given")
|
|
|
|
self._slope = slope
|
|
self._xy1 = xy1
|
|
self._xy2 = xy2
|
|
|
|
def get_transform(self):
|
|
ax = self.axes
|
|
points_transform = self._transform - ax.transData + ax.transScale
|
|
|
|
if self._xy2 is not None:
|
|
# two points were given
|
|
(x1, y1), (x2, y2) = \
|
|
points_transform.transform([self._xy1, self._xy2])
|
|
dx = x2 - x1
|
|
dy = y2 - y1
|
|
if np.allclose(x1, x2):
|
|
if np.allclose(y1, y2):
|
|
raise ValueError(
|
|
f"Cannot draw a line through two identical points "
|
|
f"(x={(x1, x2)}, y={(y1, y2)})")
|
|
slope = np.inf
|
|
else:
|
|
slope = dy / dx
|
|
else:
|
|
# one point and a slope were given
|
|
x1, y1 = points_transform.transform(self._xy1)
|
|
slope = self._slope
|
|
(vxlo, vylo), (vxhi, vyhi) = ax.transScale.transform(ax.viewLim)
|
|
# General case: find intersections with view limits in either
|
|
# direction, and draw between the middle two points.
|
|
if np.isclose(slope, 0):
|
|
start = vxlo, y1
|
|
stop = vxhi, y1
|
|
elif np.isinf(slope):
|
|
start = x1, vylo
|
|
stop = x1, vyhi
|
|
else:
|
|
_, start, stop, _ = sorted([
|
|
(vxlo, y1 + (vxlo - x1) * slope),
|
|
(vxhi, y1 + (vxhi - x1) * slope),
|
|
(x1 + (vylo - y1) / slope, vylo),
|
|
(x1 + (vyhi - y1) / slope, vyhi),
|
|
])
|
|
return (BboxTransformTo(Bbox([start, stop]))
|
|
+ ax.transLimits + ax.transAxes)
|
|
|
|
def draw(self, renderer):
|
|
self._transformed_path = None # Force regen.
|
|
super().draw(renderer)
|
|
|
|
def get_xy1(self):
|
|
"""
|
|
Return the *xy1* value of the line.
|
|
"""
|
|
return self._xy1
|
|
|
|
def get_xy2(self):
|
|
"""
|
|
Return the *xy2* value of the line.
|
|
"""
|
|
return self._xy2
|
|
|
|
def get_slope(self):
|
|
"""
|
|
Return the *slope* value of the line.
|
|
"""
|
|
return self._slope
|
|
|
|
def set_xy1(self, x, y):
|
|
"""
|
|
Set the *xy1* value of the line.
|
|
|
|
Parameters
|
|
----------
|
|
x, y : float
|
|
Points for the line to pass through.
|
|
"""
|
|
self._xy1 = x, y
|
|
|
|
def set_xy2(self, x, y):
|
|
"""
|
|
Set the *xy2* value of the line.
|
|
|
|
Parameters
|
|
----------
|
|
x, y : float
|
|
Points for the line to pass through.
|
|
"""
|
|
if self._slope is None:
|
|
self._xy2 = x, y
|
|
else:
|
|
raise ValueError("Cannot set an 'xy2' value while 'slope' is set;"
|
|
" they differ but their functionalities overlap")
|
|
|
|
def set_slope(self, slope):
|
|
"""
|
|
Set the *slope* value of the line.
|
|
|
|
Parameters
|
|
----------
|
|
slope : float
|
|
The slope of the line.
|
|
"""
|
|
if self._xy2 is None:
|
|
self._slope = slope
|
|
else:
|
|
raise ValueError("Cannot set a 'slope' value while 'xy2' is set;"
|
|
" they differ but their functionalities overlap")
|
|
|
|
|
|
class VertexSelector:
|
|
"""
|
|
Manage the callbacks to maintain a list of selected vertices for `.Line2D`.
|
|
Derived classes should override the `process_selected` method to do
|
|
something with the picks.
|
|
|
|
Here is an example which highlights the selected verts with red circles::
|
|
|
|
import numpy as np
|
|
import matplotlib.pyplot as plt
|
|
import matplotlib.lines as lines
|
|
|
|
class HighlightSelected(lines.VertexSelector):
|
|
def __init__(self, line, fmt='ro', **kwargs):
|
|
super().__init__(line)
|
|
self.markers, = self.axes.plot([], [], fmt, **kwargs)
|
|
|
|
def process_selected(self, ind, xs, ys):
|
|
self.markers.set_data(xs, ys)
|
|
self.canvas.draw()
|
|
|
|
fig, ax = plt.subplots()
|
|
x, y = np.random.rand(2, 30)
|
|
line, = ax.plot(x, y, 'bs-', picker=5)
|
|
|
|
selector = HighlightSelected(line)
|
|
plt.show()
|
|
"""
|
|
|
|
def __init__(self, line):
|
|
"""
|
|
Parameters
|
|
----------
|
|
line : `~matplotlib.lines.Line2D`
|
|
The line must already have been added to an `~.axes.Axes` and must
|
|
have its picker property set.
|
|
"""
|
|
if line.axes is None:
|
|
raise RuntimeError('You must first add the line to the Axes')
|
|
if line.get_picker() is None:
|
|
raise RuntimeError('You must first set the picker property '
|
|
'of the line')
|
|
self.axes = line.axes
|
|
self.line = line
|
|
self.cid = self.canvas.callbacks._connect_picklable(
|
|
'pick_event', self.onpick)
|
|
self.ind = set()
|
|
|
|
canvas = property(lambda self: self.axes.figure.canvas)
|
|
|
|
def process_selected(self, ind, xs, ys):
|
|
"""
|
|
Default "do nothing" implementation of the `process_selected` method.
|
|
|
|
Parameters
|
|
----------
|
|
ind : list of int
|
|
The indices of the selected vertices.
|
|
xs, ys : array-like
|
|
The coordinates of the selected vertices.
|
|
"""
|
|
pass
|
|
|
|
def onpick(self, event):
|
|
"""When the line is picked, update the set of selected indices."""
|
|
if event.artist is not self.line:
|
|
return
|
|
self.ind ^= set(event.ind)
|
|
ind = sorted(self.ind)
|
|
xdata, ydata = self.line.get_data()
|
|
self.process_selected(ind, xdata[ind], ydata[ind])
|
|
|
|
|
|
lineStyles = Line2D._lineStyles
|
|
lineMarkers = MarkerStyle.markers
|
|
drawStyles = Line2D.drawStyles
|
|
fillStyles = MarkerStyle.fillstyles
|