'
+ '
'
+ f'{color_block(self.get_outside())} outside'
+ '
'
+ '
'
+ f'bad {color_block(self.get_bad())}'
+ '
')
+
+ def copy(self):
+ """Return a copy of the colormap."""
+ return self.__copy__()
+
+
+class SegmentedBivarColormap(BivarColormap):
+ """
+ BivarColormap object generated by supersampling a regular grid.
+
+ Parameters
+ ----------
+ patch : np.array
+ Patch is required to have a shape (k, l, 3), and will get supersampled
+ to a lut of shape (N, N, 4).
+ N : int
+ The number of RGB quantization levels along each axis.
+ shape : {'square', 'circle', 'ignore', 'circleignore'}
+
+ - If 'square' each variate is clipped to [0,1] independently
+ - If 'circle' the variates are clipped radially to the center
+ of the colormap, and a circular mask is applied when the colormap
+ is displayed
+ - If 'ignore' the variates are not clipped, but instead assigned the
+ 'outside' color
+ - If 'circleignore' a circular mask is applied, but the data is not clipped
+
+ origin : (float, float)
+ The relative origin of the colormap. Typically (0, 0), for colormaps
+ that are linear on both axis, and (.5, .5) for circular colormaps.
+ Used when getting 1D colormaps from 2D colormaps.
+
+ name : str, optional
+ The name of the colormap.
+ """
+
+ def __init__(self, patch, N=256, shape='square', origin=(0, 0),
+ name='segmented bivariate colormap'):
+ _api.check_shape((None, None, 3), patch=patch)
+ self.patch = patch
+ super().__init__(N, N, shape, origin, name=name)
+
+ def _init(self):
+ s = self.patch.shape
+ _patch = np.empty((s[0], s[1], 4))
+ _patch[:, :, :3] = self.patch
+ _patch[:, :, 3] = 1
+ transform = mpl.transforms.Affine2D().translate(-0.5, -0.5)\
+ .scale(self.N / (s[1] - 1), self.N / (s[0] - 1))
+ self._lut = np.empty((self.N, self.N, 4))
+
+ _image.resample(_patch, self._lut, transform, _image.BILINEAR,
+ resample=False, alpha=1)
+ self._isinit = True
+
+
+class BivarColormapFromImage(BivarColormap):
+ """
+ BivarColormap object generated by supersampling a regular grid.
+
+ Parameters
+ ----------
+ lut : nparray of shape (N, M, 3) or (N, M, 4)
+ The look-up-table
+ shape: {'square', 'circle', 'ignore', 'circleignore'}
+
+ - If 'square' each variate is clipped to [0,1] independently
+ - If 'circle' the variates are clipped radially to the center
+ of the colormap, and a circular mask is applied when the colormap
+ is displayed
+ - If 'ignore' the variates are not clipped, but instead assigned the
+ 'outside' color
+ - If 'circleignore' a circular mask is applied, but the data is not clipped
+
+ origin: (float, float)
+ The relative origin of the colormap. Typically (0, 0), for colormaps
+ that are linear on both axis, and (.5, .5) for circular colormaps.
+ Used when getting 1D colormaps from 2D colormaps.
+ name : str, optional
+ The name of the colormap.
+
+ """
+
+ def __init__(self, lut, shape='square', origin=(0, 0), name='from image'):
+ # We can allow for a PIL.Image as input in the following way, but importing
+ # matplotlib.image.pil_to_array() results in a circular import
+ # For now, this function only accepts numpy arrays.
+ # i.e.:
+ # if isinstance(Image, lut):
+ # lut = image.pil_to_array(lut)
+ lut = np.array(lut, copy=True)
+ if lut.ndim != 3 or lut.shape[2] not in (3, 4):
+ raise ValueError("The lut must be an array of shape (n, m, 3) or (n, m, 4)",
+ " or a PIL.image encoded as RGB or RGBA")
+
+ if lut.dtype == np.uint8:
+ lut = lut.astype(np.float32)/255
+ if lut.shape[2] == 3:
+ new_lut = np.empty((lut.shape[0], lut.shape[1], 4), dtype=lut.dtype)
+ new_lut[:, :, :3] = lut
+ new_lut[:, :, 3] = 1.
+ lut = new_lut
+ self._lut = lut
+ super().__init__(lut.shape[0], lut.shape[1], shape, origin, name=name)
+
+ def _init(self):
+ self._isinit = True
+
+
class Normalize:
"""
A class which, when called, maps values within the interval
@@ -2208,8 +3097,22 @@ def rgb_to_hsv(arr):
dtype=np.promote_types(arr.dtype, np.float32), # Don't work on ints.
ndmin=2, # In case input was 1D.
)
+
out = np.zeros_like(arr)
arr_max = arr.max(-1)
+ # Check if input is in the expected range
+ if np.any(arr_max > 1):
+ raise ValueError(
+ "Input array must be in the range [0, 1]. "
+ f"Found a maximum value of {arr_max.max()}"
+ )
+
+ if arr.min() < 0:
+ raise ValueError(
+ "Input array must be in the range [0, 1]. "
+ f"Found a minimum value of {arr.min()}"
+ )
+
ipos = arr_max > 0
delta = np.ptp(arr, -1)
s = np.zeros_like(delta)
diff --git a/lib/matplotlib/colors.pyi b/lib/matplotlib/colors.pyi
index 514801b714b8..6941e3d5d176 100644
--- a/lib/matplotlib/colors.pyi
+++ b/lib/matplotlib/colors.pyi
@@ -138,6 +138,101 @@ class ListedColormap(Colormap):
def resampled(self, lutsize: int) -> ListedColormap: ...
def reversed(self, name: str | None = ...) -> ListedColormap: ...
+class MultivarColormap:
+ name: str
+ n_variates: int
+ def __init__(self, colormaps: list[Colormap], combination_mode: Literal['sRGB_add', 'sRGB_sub'], name: str = ...) -> None: ...
+ @overload
+ def __call__(
+ self, X: Sequence[Sequence[float]] | np.ndarray, alpha: ArrayLike | None = ..., bytes: bool = ..., clip: bool = ...
+ ) -> np.ndarray: ...
+ @overload
+ def __call__(
+ self, X: Sequence[float], alpha: float | None = ..., bytes: bool = ..., clip: bool = ...
+ ) -> tuple[float, float, float, float]: ...
+ @overload
+ def __call__(
+ self, X: ArrayLike, alpha: ArrayLike | None = ..., bytes: bool = ..., clip: bool = ...
+ ) -> tuple[float, float, float, float] | np.ndarray: ...
+ def copy(self) -> MultivarColormap: ...
+ def __copy__(self) -> MultivarColormap: ...
+ def __eq__(self, other: Any) -> bool: ...
+ def __getitem__(self, item: int) -> Colormap: ...
+ def __iter__(self) -> Iterator[Colormap]: ...
+ def __len__(self) -> int: ...
+ def get_bad(self) -> np.ndarray: ...
+ def resampled(self, lutshape: Sequence[int | None]) -> MultivarColormap: ...
+ def with_extremes(
+ self,
+ *,
+ bad: ColorType | None = ...,
+ under: Sequence[ColorType] | None = ...,
+ over: Sequence[ColorType] | None = ...
+ ) -> MultivarColormap: ...
+ @property
+ def combination_mode(self) -> str: ...
+ def _repr_html_(self) -> str: ...
+ def _repr_png_(self) -> bytes: ...
+
+class BivarColormap:
+ name: str
+ N: int
+ M: int
+ n_variates: int
+ def __init__(
+ self, N: int = ..., M: int | None = ..., shape: Literal['square', 'circle', 'ignore', 'circleignore'] = ...,
+ origin: Sequence[float] = ..., name: str = ...
+ ) -> None: ...
+ @overload
+ def __call__(
+ self, X: Sequence[Sequence[float]] | np.ndarray, alpha: ArrayLike | None = ..., bytes: bool = ...
+ ) -> np.ndarray: ...
+ @overload
+ def __call__(
+ self, X: Sequence[float], alpha: float | None = ..., bytes: bool = ...
+ ) -> tuple[float, float, float, float]: ...
+ @overload
+ def __call__(
+ self, X: ArrayLike, alpha: ArrayLike | None = ..., bytes: bool = ...
+ ) -> tuple[float, float, float, float] | np.ndarray: ...
+ @property
+ def lut(self) -> np.ndarray: ...
+ @property
+ def shape(self) -> str: ...
+ @property
+ def origin(self) -> tuple[float, float]: ...
+ def copy(self) -> BivarColormap: ...
+ def __copy__(self) -> BivarColormap: ...
+ def __getitem__(self, item: int) -> Colormap: ...
+ def __eq__(self, other: Any) -> bool: ...
+ def get_bad(self) -> np.ndarray: ...
+ def get_outside(self) -> np.ndarray: ...
+ def resampled(self, lutshape: Sequence[int | None], transposed: bool = ...) -> BivarColormap: ...
+ def transposed(self) -> BivarColormap: ...
+ def reversed(self, axis_0: bool = ..., axis_1: bool = ...) -> BivarColormap: ...
+ def with_extremes(
+ self,
+ *,
+ bad: ColorType | None = ...,
+ outside: ColorType | None = ...,
+ shape: str | None = ...,
+ origin: None | Sequence[float] = ...,
+ ) -> MultivarColormap: ...
+ def _repr_html_(self) -> str: ...
+ def _repr_png_(self) -> bytes: ...
+
+class SegmentedBivarColormap(BivarColormap):
+ def __init__(
+ self, patch: np.ndarray, N: int = ..., shape: Literal['square', 'circle', 'ignore', 'circleignore'] = ...,
+ origin: Sequence[float] = ..., name: str = ...
+ ) -> None: ...
+
+class BivarColormapFromImage(BivarColormap):
+ def __init__(
+ self, lut: np.ndarray, shape: Literal['square', 'circle', 'ignore', 'circleignore'] = ...,
+ origin: Sequence[float] = ..., name: str = ...
+ ) -> None: ...
+
class Normalize:
callbacks: cbook.CallbackRegistry
def __init__(
diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py
index 3b2bf3843f4d..6b685fa0ed6a 100644
--- a/lib/matplotlib/contour.py
+++ b/lib/matplotlib/contour.py
@@ -27,7 +27,7 @@
def _contour_labeler_event_handler(cs, inline, inline_spacing, event):
- canvas = cs.axes.figure.canvas
+ canvas = cs.axes.get_figure(root=True).canvas
is_button = event.name == "button_press_event"
is_key = event.name == "key_press_event"
# Quit (even if not in infinite mode; this is consistent with
@@ -199,7 +199,8 @@ def clabel(self, levels=None, *,
if not inline:
print('Remove last label by clicking third mouse button.')
mpl._blocking_input.blocking_input_loop(
- self.axes.figure, ["button_press_event", "key_press_event"],
+ self.axes.get_figure(root=True),
+ ["button_press_event", "key_press_event"],
timeout=-1, handler=functools.partial(
_contour_labeler_event_handler,
self, inline, inline_spacing))
@@ -222,8 +223,8 @@ def too_close(self, x, y, lw):
def _get_nth_label_width(self, nth):
"""Return the width of the *nth* label, in pixels."""
- fig = self.axes.figure
- renderer = fig._get_renderer()
+ fig = self.axes.get_figure(root=False)
+ renderer = fig.get_figure(root=True)._get_renderer()
return (Text(0, 0,
self.get_text(self.labelLevelList[nth], self.labelFmt),
figure=fig, fontproperties=self._label_font_props)
@@ -310,14 +311,6 @@ def _split_path_and_get_label_rotation(self, path, idx, screen_pos, lw, spacing=
determine rotation and then to break contour if desired. The extra spacing is
taken into account when breaking the path, but not when computing the angle.
"""
- if hasattr(self, "_old_style_split_collections"):
- vis = False
- for coll in self._old_style_split_collections:
- vis |= coll.get_visible()
- coll.remove()
- self.set_visible(vis)
- del self._old_style_split_collections # Invalidate them.
-
xys = path.vertices
codes = path.codes
@@ -406,97 +399,6 @@ def interp_vec(x, xp, fp): return [np.interp(x, xp, col) for col in fp.T]
return angle, Path(xys, codes)
- @_api.deprecated("3.8")
- def calc_label_rot_and_inline(self, slc, ind, lw, lc=None, spacing=5):
- """
- Calculate the appropriate label rotation given the linecontour
- coordinates in screen units, the index of the label location and the
- label width.
-
- If *lc* is not None or empty, also break contours and compute
- inlining.
-
- *spacing* is the empty space to leave around the label, in pixels.
-
- Both tasks are done together to avoid calculating path lengths
- multiple times, which is relatively costly.
-
- The method used here involves computing the path length along the
- contour in pixel coordinates and then looking approximately (label
- width / 2) away from central point to determine rotation and then to
- break contour if desired.
- """
-
- if lc is None:
- lc = []
- # Half the label width
- hlw = lw / 2.0
-
- # Check if closed and, if so, rotate contour so label is at edge
- closed = _is_closed_polygon(slc)
- if closed:
- slc = np.concatenate([slc[ind:-1], slc[:ind + 1]])
- if len(lc): # Rotate lc also if not empty
- lc = np.concatenate([lc[ind:-1], lc[:ind + 1]])
- ind = 0
-
- # Calculate path lengths
- pl = np.zeros(slc.shape[0], dtype=float)
- dx = np.diff(slc, axis=0)
- pl[1:] = np.cumsum(np.hypot(dx[:, 0], dx[:, 1]))
- pl = pl - pl[ind]
-
- # Use linear interpolation to get points around label
- xi = np.array([-hlw, hlw])
- if closed: # Look at end also for closed contours
- dp = np.array([pl[-1], 0])
- else:
- dp = np.zeros_like(xi)
-
- # Get angle of vector between the two ends of the label - must be
- # calculated in pixel space for text rotation to work correctly.
- (dx,), (dy,) = (np.diff(np.interp(dp + xi, pl, slc_col))
- for slc_col in slc.T)
- rotation = np.rad2deg(np.arctan2(dy, dx))
-
- if self.rightside_up:
- # Fix angle so text is never upside-down
- rotation = (rotation + 90) % 180 - 90
-
- # Break contour if desired
- nlc = []
- if len(lc):
- # Expand range by spacing
- xi = dp + xi + np.array([-spacing, spacing])
-
- # Get (integer) indices near points of interest; use -1 as marker
- # for out of bounds.
- I = np.interp(xi, pl, np.arange(len(pl)), left=-1, right=-1)
- I = [np.floor(I[0]).astype(int), np.ceil(I[1]).astype(int)]
- if I[0] != -1:
- xy1 = [np.interp(xi[0], pl, lc_col) for lc_col in lc.T]
- if I[1] != -1:
- xy2 = [np.interp(xi[1], pl, lc_col) for lc_col in lc.T]
-
- # Actually break contours
- if closed:
- # This will remove contour if shorter than label
- if all(i != -1 for i in I):
- nlc.append(np.vstack([xy2, lc[I[1]:I[0]+1], xy1]))
- else:
- # These will remove pieces of contour if they have length zero
- if I[0] != -1:
- nlc.append(np.vstack([lc[:I[0]+1], xy1]))
- if I[1] != -1:
- nlc.append(np.vstack([xy2, lc[I[1]:]]))
-
- # The current implementation removes contours completely
- # covered by labels. Uncomment line below to keep
- # original contour if this is the preferred behavior.
- # if not len(nlc): nlc = [lc]
-
- return rotation, nlc
-
def add_label(self, x, y, rotation, lev, cvalue):
"""Add a contour label, respecting whether *use_clabeltext* was set."""
data_x, data_y = self.axes.transData.inverted().transform((x, y))
@@ -519,12 +421,6 @@ def add_label(self, x, y, rotation, lev, cvalue):
# Add label to plot here - useful for manual mode label selection
self.axes.add_artist(t)
- @_api.deprecated("3.8", alternative="add_label")
- def add_label_clabeltext(self, x, y, rotation, lev, cvalue):
- """Add contour label with `.Text.set_transform_rotates_text`."""
- with cbook._setattr_cm(self, _use_clabeltext=True):
- self.add_label(x, y, rotation, lev, cvalue)
-
def add_label_near(self, x, y, inline=True, inline_spacing=5,
transform=None):
"""
@@ -604,15 +500,6 @@ def remove(self):
text.remove()
-def _is_closed_polygon(X):
- """
- Return whether first and last object in a sequence are the same. These are
- presumably coordinates on a polygonal curve, in which case this function
- tests if that curve is closed.
- """
- return np.allclose(X[0], X[-1], rtol=1e-10, atol=1e-13)
-
-
def _find_closest_point_on_path(xys, p):
"""
Parameters
@@ -649,7 +536,7 @@ def _find_closest_point_on_path(xys, p):
return (d2s[imin], projs[imin], (imin, imin+1))
-_docstring.interpd.update(contour_set_attributes=r"""
+_docstring.interpd.register(contour_set_attributes=r"""
Attributes
----------
ax : `~matplotlib.axes.Axes`
@@ -668,7 +555,7 @@ def _find_closest_point_on_path(xys, p):
""")
-@_docstring.dedent_interpd
+@_docstring.interpd
class ContourSet(ContourLabeler, mcoll.Collection):
"""
Store a set of contour lines or filled regions.
@@ -716,8 +603,8 @@ def __init__(self, ax, *args,
levels=None, filled=False, linewidths=None, linestyles=None,
hatches=(None,), alpha=None, origin=None, extent=None,
cmap=None, colors=None, norm=None, vmin=None, vmax=None,
- extend='neither', antialiased=None, nchunk=0, locator=None,
- transform=None, negative_linestyles=None, clip_path=None,
+ colorizer=None, extend='neither', antialiased=None, nchunk=0,
+ locator=None, transform=None, negative_linestyles=None, clip_path=None,
**kwargs):
"""
Draw contour lines or filled regions, depending on
@@ -773,6 +660,7 @@ def __init__(self, ax, *args,
alpha=alpha,
clip_path=clip_path,
transform=transform,
+ colorizer=colorizer,
)
self.axes = ax
self.levels = levels
@@ -785,6 +673,13 @@ def __init__(self, ax, *args,
self.nchunk = nchunk
self.locator = locator
+
+ if colorizer:
+ self._set_colorizer_check_keywords(colorizer, cmap=cmap,
+ norm=norm, vmin=vmin,
+ vmax=vmax, colors=colors)
+ norm = colorizer.norm
+ cmap = colorizer.cmap
if (isinstance(norm, mcolors.LogNorm)
or isinstance(self.locator, ticker.LogLocator)):
self.logscale = True
@@ -816,6 +711,11 @@ def __init__(self, ax, *args,
self._extend_min = self.extend in ['min', 'both']
self._extend_max = self.extend in ['max', 'both']
if self.colors is not None:
+ if mcolors.is_color_like(self.colors):
+ color_sequence = [self.colors]
+ else:
+ color_sequence = self.colors
+
ncolors = len(self.levels)
if self.filled:
ncolors -= 1
@@ -832,19 +732,19 @@ def __init__(self, ax, *args,
total_levels = (ncolors +
int(self._extend_min) +
int(self._extend_max))
- if (len(self.colors) == total_levels and
+ if (len(color_sequence) == total_levels and
(self._extend_min or self._extend_max)):
use_set_under_over = True
if self._extend_min:
i0 = 1
- cmap = mcolors.ListedColormap(self.colors[i0:None], N=ncolors)
+ cmap = mcolors.ListedColormap(color_sequence[i0:None], N=ncolors)
if use_set_under_over:
if self._extend_min:
- cmap.set_under(self.colors[0])
+ cmap.set_under(color_sequence[0])
if self._extend_max:
- cmap.set_over(self.colors[-1])
+ cmap.set_over(color_sequence[-1])
# label lists must be initialized here
self.labelTexts = []
@@ -906,57 +806,9 @@ def __init__(self, ax, *args,
allkinds = property(lambda self: [
[subp.codes for subp in p._iter_connected_components()]
for p in self.get_paths()])
- tcolors = _api.deprecated("3.8")(property(lambda self: [
- (tuple(rgba),) for rgba in self.to_rgba(self.cvalues, self.alpha)]))
- tlinewidths = _api.deprecated("3.8")(property(lambda self: [
- (w,) for w in self.get_linewidths()]))
alpha = property(lambda self: self.get_alpha())
linestyles = property(lambda self: self._orig_linestyles)
- @_api.deprecated("3.8", alternative="set_antialiased or get_antialiased",
- addendum="Note that get_antialiased returns an array.")
- @property
- def antialiased(self):
- return all(self.get_antialiased())
-
- @antialiased.setter
- def antialiased(self, aa):
- self.set_antialiased(aa)
-
- @_api.deprecated("3.8")
- @property
- def collections(self):
- # On access, make oneself invisible and instead add the old-style collections
- # (one PathCollection per level). We do not try to further split contours into
- # connected components as we already lost track of what pairs of contours need
- # to be considered as single units to draw filled regions with holes.
- if not hasattr(self, "_old_style_split_collections"):
- self.set_visible(False)
- fcs = self.get_facecolor()
- ecs = self.get_edgecolor()
- lws = self.get_linewidth()
- lss = self.get_linestyle()
- self._old_style_split_collections = []
- for idx, path in enumerate(self._paths):
- pc = mcoll.PathCollection(
- [path] if len(path.vertices) else [],
- alpha=self.get_alpha(),
- antialiaseds=self._antialiaseds[idx % len(self._antialiaseds)],
- transform=self.get_transform(),
- zorder=self.get_zorder(),
- label="_nolegend_",
- facecolor=fcs[idx] if len(fcs) else "none",
- edgecolor=ecs[idx] if len(ecs) else "none",
- linewidths=[lws[idx % len(lws)]],
- linestyles=[lss[idx % len(lss)]],
- )
- if self.filled:
- pc.set(hatch=self.hatches[idx % len(self.hatches)])
- self._old_style_split_collections.append(pc)
- for col in self._old_style_split_collections:
- self.axes.add_collection(col)
- return self._old_style_split_collections
-
def get_transform(self):
"""Return the `.Transform` instance used by this ContourSet."""
if self._transform is None:
@@ -1429,7 +1281,7 @@ def draw(self, renderer):
super().draw(renderer)
-@_docstring.dedent_interpd
+@_docstring.interpd
class QuadContourSet(ContourSet):
"""
Create and store a set of contour lines or filled regions.
@@ -1610,7 +1462,7 @@ def _initialize_x_y(self, z):
return np.meshgrid(x, y)
-_docstring.interpd.update(contour_doc="""
+_docstring.interpd.register(contour_doc="""
`.contour` and `.contourf` draw contour lines and filled contours,
respectively. Except as noted, function signatures and return values
are the same for both versions.
@@ -1664,10 +1516,12 @@ def _initialize_x_y(self, z):
The sequence is cycled for the levels in ascending order. If the
sequence is shorter than the number of levels, it's repeated.
- As a shortcut, single color strings may be used in place of
- one-element lists, i.e. ``'red'`` instead of ``['red']`` to color
- all levels with the same color. This shortcut does only work for
- color strings, not for other ways of specifying colors.
+ As a shortcut, a single color may be used in place of one-element lists, i.e.
+ ``'red'`` instead of ``['red']`` to color all levels with the same color.
+
+ .. versionchanged:: 3.10
+ Previously a single color had to be expressed as a string, but now any
+ valid color format may be passed.
By default (value *None*), the colormap specified by *cmap*
will be used.
@@ -1690,6 +1544,10 @@ def _initialize_x_y(self, z):
This parameter is ignored if *colors* is set.
+%(colorizer_doc)s
+
+ This parameter is ignored if *colors* is set.
+
origin : {*None*, 'upper', 'lower', 'image'}, default: None
Determines the orientation and exact position of *Z* by specifying
the position of ``Z[0, 0]``. This is only relevant, if *X*, *Y*
@@ -1735,10 +1593,10 @@ def _initialize_x_y(self, z):
An existing `.QuadContourSet` does not get notified if
properties of its colormap are changed. Therefore, an explicit
- call `.QuadContourSet.changed()` is needed after modifying the
+ call `~.ContourSet.changed()` is needed after modifying the
colormap. The explicit call can be left out, if a colorbar is
assigned to the `.QuadContourSet` because it internally calls
- `.QuadContourSet.changed()`.
+ `~.ContourSet.changed()`.
Example::
@@ -1801,7 +1659,7 @@ def _initialize_x_y(self, z):
specifies the line style for negative contours.
If *negative_linestyles* is *None*, the default is taken from
- :rc:`contour.negative_linestyles`.
+ :rc:`contour.negative_linestyle`.
*negative_linestyles* can also be an iterable of the above strings
specifying a set of linestyles to be used. If this iterable is shorter than
diff --git a/lib/matplotlib/contour.pyi b/lib/matplotlib/contour.pyi
index c386bea47ab7..7400fac50993 100644
--- a/lib/matplotlib/contour.pyi
+++ b/lib/matplotlib/contour.pyi
@@ -2,8 +2,8 @@ import matplotlib.cm as cm
from matplotlib.artist import Artist
from matplotlib.axes import Axes
from matplotlib.collections import Collection, PathCollection
+from matplotlib.colorizer import Colorizer, ColorizingArtist
from matplotlib.colors import Colormap, Normalize
-from matplotlib.font_manager import FontProperties
from matplotlib.path import Path
from matplotlib.patches import Patch
from matplotlib.text import Text
@@ -24,7 +24,7 @@ class ContourLabeler:
rightside_up: bool
labelLevelList: list[float]
labelIndiceList: list[int]
- labelMappable: cm.ScalarMappable
+ labelMappable: cm.ScalarMappable | ColorizingArtist
labelCValueList: list[ColorType]
labelXYs: list[tuple[float, float]]
def clabel(
@@ -51,20 +51,9 @@ class ContourLabeler:
def locate_label(
self, linecontour: ArrayLike, labelwidth: float
) -> tuple[float, float, float]: ...
- def calc_label_rot_and_inline(
- self,
- slc: ArrayLike,
- ind: int,
- lw: float,
- lc: ArrayLike | None = ...,
- spacing: int = ...,
- ) -> tuple[float, list[ArrayLike]]: ...
def add_label(
self, x: float, y: float, rotation: float, lev: float, cvalue: ColorType
) -> None: ...
- def add_label_clabeltext(
- self, x: float, y: float, rotation: float, lev: float, cvalue: ColorType
- ) -> None: ...
def add_label_near(
self,
x: float,
@@ -96,12 +85,6 @@ class ContourSet(ContourLabeler, Collection):
clip_path: Patch | Path | TransformedPath | TransformedPatchPath | None
labelTexts: list[Text]
labelCValues: list[ColorType]
- @property
- def tcolors(self) -> list[tuple[tuple[float, float, float, float]]]: ...
-
- # only for not filled
- @property
- def tlinewidths(self) -> list[tuple[float]]: ...
@property
def allkinds(self) -> list[list[np.ndarray | None]]: ...
@@ -110,12 +93,6 @@ class ContourSet(ContourLabeler, Collection):
@property
def alpha(self) -> float | None: ...
@property
- def antialiased(self) -> bool: ...
- @antialiased.setter
- def antialiased(self, aa: bool | Sequence[bool]) -> None: ...
- @property
- def collections(self) -> list[PathCollection]: ...
- @property
def linestyles(self) -> (
None |
Literal["solid", "dashed", "dashdot", "dotted"] |
@@ -141,6 +118,7 @@ class ContourSet(ContourLabeler, Collection):
norm: str | Normalize | None = ...,
vmin: float | None = ...,
vmax: float | None = ...,
+ colorizer: Colorizer | None = ...,
extend: Literal["neither", "both", "min", "max"] = ...,
antialiased: bool | None = ...,
nchunk: int = ...,
diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py
index c12d9f31ba4b..511e1c6df6cc 100644
--- a/lib/matplotlib/dates.py
+++ b/lib/matplotlib/dates.py
@@ -37,7 +37,7 @@
is achievable for (approximately) 70 years on either side of the epoch, and
20 microseconds for the rest of the allowable range of dates (year 0001 to
9999). The epoch can be changed at import time via `.dates.set_epoch` or
-:rc:`dates.epoch` to other dates if necessary; see
+:rc:`date.epoch` to other dates if necessary; see
:doc:`/gallery/ticks/date_precision_and_epochs` for a discussion.
.. note::
@@ -267,7 +267,7 @@ def set_epoch(epoch):
"""
Set the epoch (origin for dates) for datetime calculations.
- The default epoch is :rc:`dates.epoch` (by default 1970-01-01T00:00).
+ The default epoch is :rc:`date.epoch`.
If microsecond accuracy is desired, the date being plotted needs to be
within approximately 70 years of the epoch. Matplotlib internally
@@ -796,7 +796,12 @@ def format_ticks(self, values):
if show_offset:
# set the offset string:
- self.offset_string = tickdatetime[-1].strftime(offsetfmts[level])
+ if (self._locator.axis and
+ self._locator.axis.__name__ in ('xaxis', 'yaxis')
+ and self._locator.axis.get_inverted()):
+ self.offset_string = tickdatetime[0].strftime(offsetfmts[level])
+ else:
+ self.offset_string = tickdatetime[-1].strftime(offsetfmts[level])
if self._usetex:
self.offset_string = _wrap_in_tex(self.offset_string)
else:
diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py
index 82f43b56292d..bd21367ce73d 100644
--- a/lib/matplotlib/dviread.py
+++ b/lib/matplotlib/dviread.py
@@ -132,20 +132,20 @@ def glyph_name_or_index(self):
# raw: Return delta as is.
raw=lambda dvi, delta: delta,
# u1: Read 1 byte as an unsigned number.
- u1=lambda dvi, delta: dvi._arg(1, signed=False),
+ u1=lambda dvi, delta: dvi._read_arg(1, signed=False),
# u4: Read 4 bytes as an unsigned number.
- u4=lambda dvi, delta: dvi._arg(4, signed=False),
+ u4=lambda dvi, delta: dvi._read_arg(4, signed=False),
# s4: Read 4 bytes as a signed number.
- s4=lambda dvi, delta: dvi._arg(4, signed=True),
+ s4=lambda dvi, delta: dvi._read_arg(4, signed=True),
# slen: Read delta bytes as a signed number, or None if delta is None.
- slen=lambda dvi, delta: dvi._arg(delta, signed=True) if delta else None,
+ slen=lambda dvi, delta: dvi._read_arg(delta, signed=True) if delta else None,
# slen1: Read (delta + 1) bytes as a signed number.
- slen1=lambda dvi, delta: dvi._arg(delta + 1, signed=True),
+ slen1=lambda dvi, delta: dvi._read_arg(delta + 1, signed=True),
# ulen1: Read (delta + 1) bytes as an unsigned number.
- ulen1=lambda dvi, delta: dvi._arg(delta + 1, signed=False),
+ ulen1=lambda dvi, delta: dvi._read_arg(delta + 1, signed=False),
# olen1: Read (delta + 1) bytes as an unsigned number if less than 4 bytes,
# as a signed number if 4 bytes.
- olen1=lambda dvi, delta: dvi._arg(delta + 1, signed=(delta == 3)),
+ olen1=lambda dvi, delta: dvi._read_arg(delta + 1, signed=(delta == 3)),
)
@@ -230,6 +230,7 @@ def __init__(self, filename, dpi):
self.dpi = dpi
self.fonts = {}
self.state = _dvistate.pre
+ self._missing_font = None
def __enter__(self):
"""Context manager enter method, does nothing."""
@@ -270,7 +271,8 @@ def _output(self):
Output the text and boxes belonging to the most recent page.
page = dvi._output()
"""
- minx, miny, maxx, maxy = np.inf, np.inf, -np.inf, -np.inf
+ minx = miny = np.inf
+ maxx = maxy = -np.inf
maxy_pure = -np.inf
for elt in self.text + self.boxes:
if isinstance(elt, Box):
@@ -337,6 +339,8 @@ def _read(self):
while True:
byte = self.file.read(1)[0]
self._dtable[byte](self, byte)
+ if self._missing_font:
+ raise self._missing_font.to_exception()
name = self._dtable[byte].__name__
if name == "_push":
down_stack.append(down_stack[-1])
@@ -354,7 +358,7 @@ def _read(self):
self.close()
return False
- def _arg(self, nbytes, signed=False):
+ def _read_arg(self, nbytes, signed=False):
"""
Read and return a big-endian integer *nbytes* long.
Signedness is determined by the *signed* keyword.
@@ -364,11 +368,15 @@ def _arg(self, nbytes, signed=False):
@_dispatch(min=0, max=127, state=_dvistate.inpage)
def _set_char_immediate(self, char):
self._put_char_real(char)
+ if isinstance(self.fonts[self.f], cbook._ExceptionInfo):
+ return
self.h += self.fonts[self.f]._width_of(char)
@_dispatch(min=128, max=131, state=_dvistate.inpage, args=('olen1',))
def _set_char(self, char):
self._put_char_real(char)
+ if isinstance(self.fonts[self.f], cbook._ExceptionInfo):
+ return
self.h += self.fonts[self.f]._width_of(char)
@_dispatch(132, state=_dvistate.inpage, args=('s4', 's4'))
@@ -382,7 +390,9 @@ def _put_char(self, char):
def _put_char_real(self, char):
font = self.fonts[self.f]
- if font._vf is None:
+ if isinstance(font, cbook._ExceptionInfo):
+ self._missing_font = font
+ elif font._vf is None:
self.text.append(Text(self.h, self.v, font, char,
font._width_of(char)))
else:
@@ -413,7 +423,7 @@ def _nop(self, _):
@_dispatch(139, state=_dvistate.outer, args=('s4',)*11)
def _bop(self, c0, c1, c2, c3, c4, c5, c6, c7, c8, c9, p):
self.state = _dvistate.inpage
- self.h, self.v, self.w, self.x, self.y, self.z = 0, 0, 0, 0, 0, 0
+ self.h = self.v = self.w = self.x = self.y = self.z = 0
self.stack = []
self.text = [] # list of Text objects
self.boxes = [] # list of Box objects
@@ -486,9 +496,18 @@ def _fnt_def(self, k, c, s, d, a, l):
def _fnt_def_real(self, k, c, s, d, a, l):
n = self.file.read(a + l)
fontname = n[-l:].decode('ascii')
- tfm = _tfmfile(fontname)
+ try:
+ tfm = _tfmfile(fontname)
+ except FileNotFoundError as exc:
+ # Explicitly allow defining missing fonts for Vf support; we only
+ # register an error when trying to load a glyph from a missing font
+ # and throw that error in Dvi._read. For Vf, _finalize_packet
+ # checks whether a missing glyph has been used, and in that case
+ # skips the glyph definition.
+ self.fonts[k] = cbook._ExceptionInfo.from_exception(exc)
+ return
if c != 0 and tfm.checksum != 0 and c != tfm.checksum:
- raise ValueError('tfm checksum mismatch: %s' % n)
+ raise ValueError(f'tfm checksum mismatch: {n}')
try:
vf = _vffile(fontname)
except FileNotFoundError:
@@ -499,7 +518,7 @@ def _fnt_def_real(self, k, c, s, d, a, l):
def _pre(self, i, num, den, mag, k):
self.file.read(k) # comment in the dvi file
if i != 2:
- raise ValueError("Unknown dvi format %d" % i)
+ raise ValueError(f"Unknown dvi format {i}")
if num != 25400000 or den != 7227 * 2**16:
raise ValueError("Nonstandard units in dvi file")
# meaning: TeX always uses those exact values, so it
@@ -660,8 +679,8 @@ def _read(self):
Read one page from the file. Return True if successful,
False if there were no more pages.
"""
- packet_char, packet_ends = None, None
- packet_len, packet_width = None, None
+ packet_char = packet_ends = None
+ packet_len = packet_width = None
while True:
byte = self.file.read(1)[0]
# If we are in a packet, execute the dvi instructions
@@ -669,62 +688,73 @@ def _read(self):
byte_at = self.file.tell()-1
if byte_at == packet_ends:
self._finalize_packet(packet_char, packet_width)
- packet_len, packet_char, packet_width = None, None, None
+ packet_len = packet_char = packet_width = None
# fall through to out-of-packet code
elif byte_at > packet_ends:
raise ValueError("Packet length mismatch in vf file")
else:
if byte in (139, 140) or byte >= 243:
- raise ValueError(
- "Inappropriate opcode %d in vf file" % byte)
+ raise ValueError(f"Inappropriate opcode {byte} in vf file")
Dvi._dtable[byte](self, byte)
continue
# We are outside a packet
if byte < 242: # a short packet (length given by byte)
packet_len = byte
- packet_char, packet_width = self._arg(1), self._arg(3)
+ packet_char = self._read_arg(1)
+ packet_width = self._read_arg(3)
packet_ends = self._init_packet(byte)
self.state = _dvistate.inpage
elif byte == 242: # a long packet
- packet_len, packet_char, packet_width = \
- [self._arg(x) for x in (4, 4, 4)]
+ packet_len = self._read_arg(4)
+ packet_char = self._read_arg(4)
+ packet_width = self._read_arg(4)
self._init_packet(packet_len)
elif 243 <= byte <= 246:
- k = self._arg(byte - 242, byte == 246)
- c, s, d, a, l = [self._arg(x) for x in (4, 4, 4, 1, 1)]
+ k = self._read_arg(byte - 242, byte == 246)
+ c = self._read_arg(4)
+ s = self._read_arg(4)
+ d = self._read_arg(4)
+ a = self._read_arg(1)
+ l = self._read_arg(1)
self._fnt_def_real(k, c, s, d, a, l)
if self._first_font is None:
self._first_font = k
elif byte == 247: # preamble
- i, k = self._arg(1), self._arg(1)
+ i = self._read_arg(1)
+ k = self._read_arg(1)
x = self.file.read(k)
- cs, ds = self._arg(4), self._arg(4)
+ cs = self._read_arg(4)
+ ds = self._read_arg(4)
self._pre(i, x, cs, ds)
elif byte == 248: # postamble (just some number of 248s)
break
else:
- raise ValueError("Unknown vf opcode %d" % byte)
+ raise ValueError(f"Unknown vf opcode {byte}")
def _init_packet(self, pl):
if self.state != _dvistate.outer:
raise ValueError("Misplaced packet in vf file")
- self.h, self.v, self.w, self.x, self.y, self.z = 0, 0, 0, 0, 0, 0
- self.stack, self.text, self.boxes = [], [], []
+ self.h = self.v = self.w = self.x = self.y = self.z = 0
+ self.stack = []
+ self.text = []
+ self.boxes = []
self.f = self._first_font
+ self._missing_font = None
return self.file.tell() + pl
def _finalize_packet(self, packet_char, packet_width):
- self._chars[packet_char] = Page(
- text=self.text, boxes=self.boxes, width=packet_width,
- height=None, descent=None)
+ if not self._missing_font: # Otherwise we don't have full glyph definition.
+ self._chars[packet_char] = Page(
+ text=self.text, boxes=self.boxes, width=packet_width,
+ height=None, descent=None)
self.state = _dvistate.outer
def _pre(self, i, x, cs, ds):
if self.state is not _dvistate.pre:
raise ValueError("pre command in middle of vf file")
if i != 202:
- raise ValueError("Unknown vf format %d" % i)
+ raise ValueError(f"Unknown vf format {i}")
if len(x):
_log.debug('vf file comment: %s', x)
self.state = _dvistate.outer
@@ -774,7 +804,9 @@ def __init__(self, filename):
widths = struct.unpack(f'!{nw}i', file.read(4*nw))
heights = struct.unpack(f'!{nh}i', file.read(4*nh))
depths = struct.unpack(f'!{nd}i', file.read(4*nd))
- self.width, self.height, self.depth = {}, {}, {}
+ self.width = {}
+ self.height = {}
+ self.depth = {}
for idx, char in enumerate(range(bc, ec+1)):
byte0 = char_info[4*idx]
byte1 = char_info[4*idx+1]
diff --git a/lib/matplotlib/dviread.pyi b/lib/matplotlib/dviread.pyi
index bf5cfcbe317a..270818278f17 100644
--- a/lib/matplotlib/dviread.pyi
+++ b/lib/matplotlib/dviread.pyi
@@ -5,6 +5,7 @@ from enum import Enum
from collections.abc import Generator
from typing import NamedTuple
+from typing_extensions import Self # < Py 3.11
class _dvistate(Enum):
pre: int
@@ -47,8 +48,7 @@ class Dvi:
fonts: dict[int, DviFont]
state: _dvistate
def __init__(self, filename: str | os.PathLike, dpi: float | None) -> None: ...
- # Replace return with Self when py3.9 is dropped
- def __enter__(self) -> Dvi: ...
+ def __enter__(self) -> Self: ...
def __exit__(self, etype, evalue, etrace) -> None: ...
def __iter__(self) -> Generator[Page, None, None]: ...
def close(self) -> None: ...
@@ -83,8 +83,7 @@ class PsFont(NamedTuple):
filename: str
class PsfontsMap:
- # Replace return with Self when py3.9 is dropped
- def __new__(cls, filename: str | os.PathLike) -> PsfontsMap: ...
+ def __new__(cls, filename: str | os.PathLike) -> Self: ...
def __getitem__(self, texname: bytes) -> PsFont: ...
def find_tex_file(filename: str | os.PathLike) -> str: ...
diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py
index 0d939190a0a9..3d6f9a7f4c16 100644
--- a/lib/matplotlib/figure.py
+++ b/lib/matplotlib/figure.py
@@ -6,9 +6,8 @@
Many methods are implemented in `FigureBase`.
`SubFigure`
- A logical figure inside a figure, usually added to a figure (or parent
- `SubFigure`) with `Figure.add_subfigure` or `Figure.subfigures` methods
- (provisional API v3.4).
+ A logical figure inside a figure, usually added to a figure (or parent `SubFigure`)
+ with `Figure.add_subfigure` or `Figure.subfigures` methods.
Figures are typically created using pyplot methods `~.pyplot.figure`,
`~.pyplot.subplots`, and `~.pyplot.subplot_mosaic`.
@@ -30,6 +29,7 @@
from contextlib import ExitStack
import inspect
import itertools
+import functools
import logging
from numbers import Integral
import threading
@@ -63,8 +63,8 @@
def _stale_figure_callback(self, val):
- if self.figure:
- self.figure.stale = val
+ if (fig := self.get_figure(root=False)) is not None:
+ fig.stale = val
class _AxesStack:
@@ -194,14 +194,15 @@ def autofmt_xdate(
Selects which ticklabels to rotate.
"""
_api.check_in_list(['major', 'minor', 'both'], which=which)
- allsubplots = all(ax.get_subplotspec() for ax in self.axes)
- if len(self.axes) == 1:
+ axes = [ax for ax in self.axes if ax._label != '