From 8a1f201c898aa7a509ba1d56670976ba51b0155b Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 9 Apr 2026 22:35:13 +0200 Subject: [PATCH 001/215] TYP: Add type information to the data parameter of plot functions Closes #31480. We type all as `Mapping[str, Any]`. Some uses have actually a broad range of supported types because they map quite a number of parameters. Even for the ones which we know all parameters supported by `data` are ArrayLike, typing as `Mapping[str, ArrayLike]` may be too risky, as the user may have more keys in the data structure, which we don't use, but their values would be type checked and could be anything. --- lib/matplotlib/axes/_axes.pyi | 81 ++++++++++++------------- lib/matplotlib/pyplot.py | 107 +++++++++++++++++----------------- lib/matplotlib/typing.py | 5 +- 3 files changed, 99 insertions(+), 94 deletions(-) diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 09587ab753a3..3c222bd6bc1d 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -37,7 +37,8 @@ from collections.abc import Callable, Iterable, Sequence from typing import Any, Literal, overload import numpy as np from numpy.typing import ArrayLike -from matplotlib.typing import ColorType, MarkerType, LegendLocType, LineStyleType +from matplotlib.typing import ( + ColorType, DataParamType, MarkerType, LegendLocType, LineStyleType) import pandas as pd @@ -172,7 +173,7 @@ class Axes(_AxesBase): linestyles: LineStyleType = ..., *, label: str = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> LineCollection: ... def vlines( @@ -184,7 +185,7 @@ class Axes(_AxesBase): linestyles: LineStyleType = ..., *, label: str = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> LineCollection: ... def eventplot( @@ -198,7 +199,7 @@ class Axes(_AxesBase): colors: ColorType | Sequence[ColorType] | None = ..., alpha: float | Sequence[float] | None = ..., linestyles: LineStyleType | Sequence[LineStyleType] = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> EventCollection: ... def plot( @@ -206,14 +207,14 @@ class Axes(_AxesBase): *args: float | ArrayLike | str, scalex: bool = ..., scaley: bool = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> list[Line2D]: ... def loglog(self, *args, **kwargs) -> list[Line2D]: ... def semilogx(self, *args, **kwargs) -> list[Line2D]: ... def semilogy(self, *args, **kwargs) -> list[Line2D]: ... def acorr( - self, x: ArrayLike, *, data=..., **kwargs + self, x: ArrayLike, *, data: DataParamType = ..., **kwargs ) -> tuple[np.ndarray, np.ndarray, LineCollection | Line2D, Line2D | None]: ... def xcorr( self, @@ -224,7 +225,7 @@ class Axes(_AxesBase): detrend: Callable[[ArrayLike], ArrayLike] = ..., usevlines: bool = ..., maxlags: int = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> tuple[np.ndarray, np.ndarray, LineCollection | Line2D, Line2D | None]: ... def step( @@ -233,7 +234,7 @@ class Axes(_AxesBase): y: ArrayLike, *args, where: Literal["pre", "post", "mid"] = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> list[Line2D]: ... def bar( @@ -244,7 +245,7 @@ class Axes(_AxesBase): bottom: float | ArrayLike | None = ..., *, align: Literal["center", "edge"] = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> BarContainer: ... def barh( @@ -255,7 +256,7 @@ class Axes(_AxesBase): left: float | ArrayLike | None = ..., *, align: Literal["center", "edge"] = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> BarContainer: ... def bar_label( @@ -274,7 +275,7 @@ class Axes(_AxesBase): yrange: tuple[float, float], align: Literal["bottom", "center", "top"] = ..., *, - data=..., + data: DataParamType = ..., **kwargs ) -> PolyCollection: ... def grouped_bar( @@ -299,7 +300,7 @@ class Axes(_AxesBase): bottom: float = ..., label: str | None = ..., orientation: Literal["vertical", "horizontal"] = ..., - data=..., + data: DataParamType = ..., ) -> StemContainer: ... # TODO: data kwarg preprocessor? @@ -324,7 +325,7 @@ class Axes(_AxesBase): rotatelabels: bool = ..., normalize: bool = ..., hatch: str | Sequence[str] | None = ..., - data=..., + data: DataParamType = ..., ) -> PieContainer: ... def pie_label( self, @@ -356,7 +357,7 @@ class Axes(_AxesBase): xuplims: bool | ArrayLike = ..., errorevery: int | tuple[int, int] = ..., capthick: float | None = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> ErrorbarContainer: ... def boxplot( @@ -391,7 +392,7 @@ class Axes(_AxesBase): zorder: float | None = ..., capwidths: float | ArrayLike | None = ..., label: Sequence[str] | None = ..., - data=..., + data: DataParamType = ..., ) -> dict[str, Any]: ... def bxp( self, @@ -436,7 +437,7 @@ class Axes(_AxesBase): edgecolors: Literal["face", "none"] | ColorType | Sequence[ColorType] | None = ..., colorizer: Colorizer | None = ..., plotnonfinite: bool = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> PathCollection: ... def hexbin( @@ -461,7 +462,7 @@ class Axes(_AxesBase): mincnt: int | None = ..., marginals: bool = ..., colorizer: Colorizer | None = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> PolyCollection: ... def arrow( @@ -470,9 +471,9 @@ class Axes(_AxesBase): def quiverkey( self, Q: Quiver, X: float, Y: float, U: float, label: str, **kwargs ) -> QuiverKey: ... - def quiver(self, *args, data=..., **kwargs) -> Quiver: ... - def barbs(self, *args, data=..., **kwargs) -> Barbs: ... - def fill(self, *args, data=..., **kwargs) -> list[Polygon]: ... + def quiver(self, *args, data: DataParamType = ..., **kwargs) -> Quiver: ... + def barbs(self, *args, data: DataParamType = ..., **kwargs) -> Barbs: ... + def fill(self, *args, data: DataParamType = ..., **kwargs) -> list[Polygon]: ... def fill_between( self, x: ArrayLike, @@ -482,7 +483,7 @@ class Axes(_AxesBase): interpolate: bool = ..., step: Literal["pre", "post", "mid"] | None = ..., *, - data=..., + data: DataParamType = ..., **kwargs ) -> FillBetweenPolyCollection: ... def fill_betweenx( @@ -494,7 +495,7 @@ class Axes(_AxesBase): step: Literal["pre", "post", "mid"] | None = ..., interpolate: bool = ..., *, - data=..., + data: DataParamType = ..., **kwargs ) -> FillBetweenPolyCollection: ... def imshow( @@ -516,7 +517,7 @@ class Axes(_AxesBase): filterrad: float = ..., resample: bool | None = ..., url: str | None = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> AxesImage: ... def pcolor( @@ -529,7 +530,7 @@ class Axes(_AxesBase): vmin: float | None = ..., vmax: float | None = ..., colorizer: Colorizer | None = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> Collection: ... def pcolormesh( @@ -543,7 +544,7 @@ class Axes(_AxesBase): colorizer: Colorizer | None = ..., shading: Literal["flat", "nearest", "gouraud", "auto"] | None = ..., antialiased: bool = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> QuadMesh: ... def pcolorfast( @@ -555,11 +556,11 @@ class Axes(_AxesBase): vmin: float | None = ..., vmax: float | None = ..., colorizer: Colorizer | None = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> AxesImage | PcolorImage | QuadMesh: ... - def contour(self, *args, data=..., **kwargs) -> QuadContourSet: ... - def contourf(self, *args, data=..., **kwargs) -> QuadContourSet: ... + def contour(self, *args, data: DataParamType = ..., **kwargs) -> QuadContourSet: ... + def contourf(self, *args, data: DataParamType = ..., **kwargs) -> QuadContourSet: ... def clabel( self, CS: ContourSet, levels: ArrayLike | None = ..., **kwargs ) -> list[Text]: ... @@ -581,7 +582,7 @@ class Axes(_AxesBase): color: ColorType | Sequence[ColorType] | None = ..., label: str | Sequence[str] | None = ..., stacked: bool = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> tuple[ np.ndarray | list[np.ndarray], @@ -596,7 +597,7 @@ class Axes(_AxesBase): orientation: Literal["vertical", "horizontal"] = ..., baseline: float | ArrayLike | None = ..., fill: bool = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> StepPatch: ... def hist2d( @@ -614,7 +615,7 @@ class Axes(_AxesBase): weights: ArrayLike | None = ..., cmin: float | None = ..., cmax: float | None = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> tuple[np.ndarray, np.ndarray, np.ndarray, QuadMesh]: ... def ecdf( @@ -625,7 +626,7 @@ class Axes(_AxesBase): complementary: bool=..., orientation: Literal["vertical", "horizontal"]=..., compress: bool=..., - data=..., + data: DataParamType = ..., **kwargs ) -> Line2D: ... def psd( @@ -644,7 +645,7 @@ class Axes(_AxesBase): sides: Literal["default", "onesided", "twosided"] | None = ..., scale_by_freq: bool | None = ..., return_line: bool | None = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, Line2D]: ... def csd( @@ -664,7 +665,7 @@ class Axes(_AxesBase): sides: Literal["default", "onesided", "twosided"] | None = ..., scale_by_freq: bool | None = ..., return_line: bool | None = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, Line2D]: ... def magnitude_spectrum( @@ -677,7 +678,7 @@ class Axes(_AxesBase): pad_to: int | None = ..., sides: Literal["default", "onesided", "twosided"] | None = ..., scale: Literal["default", "linear", "dB"] | None = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> tuple[np.ndarray, np.ndarray, Line2D]: ... def angle_spectrum( @@ -689,7 +690,7 @@ class Axes(_AxesBase): window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ..., pad_to: int | None = ..., sides: Literal["default", "onesided", "twosided"] | None = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> tuple[np.ndarray, np.ndarray, Line2D]: ... def phase_spectrum( @@ -701,7 +702,7 @@ class Axes(_AxesBase): window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ..., pad_to: int | None = ..., sides: Literal["default", "onesided", "twosided"] | None = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> tuple[np.ndarray, np.ndarray, Line2D]: ... def cohere( @@ -719,7 +720,7 @@ class Axes(_AxesBase): pad_to: int | None = ..., sides: Literal["default", "onesided", "twosided"] = ..., scale_by_freq: bool | None = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> tuple[np.ndarray, np.ndarray]: ... def specgram( @@ -743,7 +744,7 @@ class Axes(_AxesBase): scale: Literal["default", "linear", "dB"] | None = ..., vmin: float | None = ..., vmax: float | None = ..., - data=..., + data: DataParamType = ..., **kwargs ) -> tuple[np.ndarray, np.ndarray, np.ndarray, AxesImage]: ... def spy( @@ -778,7 +779,7 @@ class Axes(_AxesBase): side: Literal["both", "low", "high"] = ..., facecolor: Sequence[ColorType] | ColorType | None = ..., linecolor: Sequence[ColorType] | ColorType | None = ..., - data=..., + data: DataParamType = ..., ) -> dict[str, Collection]: ... def violin( self, diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index ed7821ca1b27..777bc4877aa9 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -140,6 +140,7 @@ CloseEventType, ColorType, CoordsType, + DataParamType, DrawEventType, HashableList, KeyEventType, @@ -2999,7 +3000,7 @@ def waitforbuttonpress(timeout: float = -1) -> None | bool: # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.acorr) def acorr( - x: ArrayLike, *, data=None, **kwargs + x: ArrayLike, *, data: DataParamType = None, **kwargs ) -> tuple[np.ndarray, np.ndarray, LineCollection | Line2D, Line2D | None]: return gca().acorr(x, **({"data": data} if data is not None else {}), **kwargs) @@ -3014,7 +3015,7 @@ def angle_spectrum( pad_to: int | None = None, sides: Literal["default", "onesided", "twosided"] | None = None, *, - data=None, + data: DataParamType = None, **kwargs, ) -> tuple[np.ndarray, np.ndarray, Line2D]: return gca().angle_spectrum( @@ -3130,7 +3131,7 @@ def bar( bottom: float | ArrayLike | None = None, *, align: Literal["center", "edge"] = "center", - data=None, + data: DataParamType = None, **kwargs, ) -> BarContainer: return gca().bar( @@ -3146,7 +3147,7 @@ def bar( # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.barbs) -def barbs(*args, data=None, **kwargs) -> Barbs: +def barbs(*args, data: DataParamType = None, **kwargs) -> Barbs: return gca().barbs(*args, **({"data": data} if data is not None else {}), **kwargs) @@ -3159,7 +3160,7 @@ def barh( left: float | ArrayLike | None = None, *, align: Literal["center", "edge"] = "center", - data=None, + data: DataParamType = None, **kwargs, ) -> BarContainer: return gca().barh( @@ -3227,7 +3228,7 @@ def boxplot( capwidths: float | ArrayLike | None = None, label: Sequence[str] | None = None, *, - data=None, + data: DataParamType = None, ) -> dict[str, Any]: return gca().boxplot( x, @@ -3270,7 +3271,7 @@ def broken_barh( yrange: tuple[float, float], align: Literal["bottom", "center", "top"] = "bottom", *, - data=None, + data: DataParamType = None, **kwargs, ) -> PolyCollection: return gca().broken_barh( @@ -3296,16 +3297,15 @@ def cohere( NFFT: int = 256, Fs: float = 2, Fc: int = 0, - detrend: ( - Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] - ) = mlab.detrend_none, + detrend: Literal["none", "mean", "linear"] + | Callable[[ArrayLike], ArrayLike] = mlab.detrend_none, window: Callable[[ArrayLike], ArrayLike] | ArrayLike = mlab.window_hanning, noverlap: int = 0, pad_to: int | None = None, sides: Literal["default", "onesided", "twosided"] = "default", scale_by_freq: bool | None = None, *, - data=None, + data: DataParamType = None, **kwargs, ) -> tuple[np.ndarray, np.ndarray]: return gca().cohere( @@ -3327,7 +3327,7 @@ def cohere( # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.contour) -def contour(*args, data=None, **kwargs) -> QuadContourSet: +def contour(*args, data: DataParamType = None, **kwargs) -> QuadContourSet: __ret = gca().contour( *args, **({"data": data} if data is not None else {}), **kwargs ) @@ -3338,7 +3338,7 @@ def contour(*args, data=None, **kwargs) -> QuadContourSet: # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.contourf) -def contourf(*args, data=None, **kwargs) -> QuadContourSet: +def contourf(*args, data: DataParamType = None, **kwargs) -> QuadContourSet: __ret = gca().contourf( *args, **({"data": data} if data is not None else {}), **kwargs ) @@ -3355,9 +3355,9 @@ def csd( NFFT: int | None = None, Fs: float | None = None, Fc: int | None = None, - detrend: ( - Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] | None - ) = None, + detrend: Literal["none", "mean", "linear"] + | Callable[[ArrayLike], ArrayLike] + | None = None, window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = None, noverlap: int | None = None, pad_to: int | None = None, @@ -3365,7 +3365,7 @@ def csd( scale_by_freq: bool | None = None, return_line: bool | None = None, *, - data=None, + data: DataParamType = None, **kwargs, ) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, Line2D]: return gca().csd( @@ -3395,7 +3395,7 @@ def ecdf( complementary: bool = False, orientation: Literal["vertical", "horizontal"] = "vertical", compress: bool = False, - data=None, + data: DataParamType = None, **kwargs, ) -> Line2D: return gca().ecdf( @@ -3429,7 +3429,7 @@ def errorbar( capthick: float | None = None, elinestyle: LineStyleType | None = None, *, - data=None, + data: DataParamType = None, **kwargs, ) -> ErrorbarContainer: return gca().errorbar( @@ -3466,7 +3466,7 @@ def eventplot( alpha: float | Sequence[float] | None = None, linestyles: LineStyleType | Sequence[LineStyleType] = "solid", *, - data=None, + data: DataParamType = None, **kwargs, ) -> EventCollection: return gca().eventplot( @@ -3485,7 +3485,7 @@ def eventplot( # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.fill) -def fill(*args, data=None, **kwargs) -> list[Polygon]: +def fill(*args, data: DataParamType = None, **kwargs) -> list[Polygon]: return gca().fill(*args, **({"data": data} if data is not None else {}), **kwargs) @@ -3499,7 +3499,7 @@ def fill_between( interpolate: bool = False, step: Literal["pre", "post", "mid"] | None = None, *, - data=None, + data: DataParamType = None, **kwargs, ) -> FillBetweenPolyCollection: return gca().fill_between( @@ -3524,7 +3524,7 @@ def fill_betweenx( step: Literal["pre", "post", "mid"] | None = None, interpolate: bool = False, *, - data=None, + data: DataParamType = None, **kwargs, ) -> FillBetweenPolyCollection: return gca().fill_betweenx( @@ -3600,7 +3600,7 @@ def hexbin( marginals: bool = False, colorizer: Colorizer | None = None, *, - data=None, + data: DataParamType = None, **kwargs, ) -> PolyCollection: __ret = gca().hexbin( @@ -3649,7 +3649,7 @@ def hist( label: str | Sequence[str] | None = None, stacked: bool = False, *, - data=None, + data: DataParamType = None, **kwargs, ) -> tuple[ np.ndarray | list[np.ndarray], @@ -3686,7 +3686,7 @@ def stairs( orientation: Literal["vertical", "horizontal"] = "vertical", baseline: float | ArrayLike | None = 0, fill: bool = False, - data=None, + data: DataParamType = None, **kwargs, ) -> StepPatch: return gca().stairs( @@ -3712,7 +3712,7 @@ def hist2d( cmin: float | None = None, cmax: float | None = None, *, - data=None, + data: DataParamType = None, **kwargs, ) -> tuple[np.ndarray, np.ndarray, np.ndarray, QuadMesh]: __ret = gca().hist2d( @@ -3741,7 +3741,7 @@ def hlines( linestyles: LineStyleType = "solid", label: str = "", *, - data=None, + data: DataParamType = None, **kwargs, ) -> LineCollection: return gca().hlines( @@ -3776,7 +3776,7 @@ def imshow( filterrad: float = 4.0, resample: bool | None = None, url: str | None = None, - data=None, + data: DataParamType = None, **kwargs, ) -> AxesImage: __ret = gca().imshow( @@ -3834,7 +3834,7 @@ def magnitude_spectrum( sides: Literal["default", "onesided", "twosided"] | None = None, scale: Literal["default", "linear", "dB"] | None = None, *, - data=None, + data: DataParamType = None, **kwargs, ) -> tuple[np.ndarray, np.ndarray, Line2D]: return gca().magnitude_spectrum( @@ -3884,7 +3884,7 @@ def pcolor( vmin: float | None = None, vmax: float | None = None, colorizer: Colorizer | None = None, - data=None, + data: DataParamType = None, **kwargs, ) -> Collection: __ret = gca().pcolor( @@ -3915,7 +3915,7 @@ def pcolormesh( colorizer: Colorizer | None = None, shading: Literal["flat", "nearest", "gouraud", "auto"] | None = None, antialiased: bool = False, - data=None, + data: DataParamType = None, **kwargs, ) -> QuadMesh: __ret = gca().pcolormesh( @@ -3945,7 +3945,7 @@ def phase_spectrum( pad_to: int | None = None, sides: Literal["default", "onesided", "twosided"] | None = None, *, - data=None, + data: DataParamType = None, **kwargs, ) -> tuple[np.ndarray, np.ndarray, Line2D]: return gca().phase_spectrum( @@ -3982,7 +3982,7 @@ def pie( *, normalize: bool = True, hatch: str | Sequence[str] | None = None, - data=None, + data: DataParamType = None, ) -> PieContainer: return gca().pie( x, @@ -4035,7 +4035,7 @@ def plot( *args: float | ArrayLike | str, scalex: bool = True, scaley: bool = True, - data=None, + data: DataParamType = None, **kwargs, ) -> list[Line2D]: return gca().plot( @@ -4054,9 +4054,9 @@ def psd( NFFT: int | None = None, Fs: float | None = None, Fc: int | None = None, - detrend: ( - Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] | None - ) = None, + detrend: Literal["none", "mean", "linear"] + | Callable[[ArrayLike], ArrayLike] + | None = None, window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = None, noverlap: int | None = None, pad_to: int | None = None, @@ -4064,7 +4064,7 @@ def psd( scale_by_freq: bool | None = None, return_line: bool | None = None, *, - data=None, + data: DataParamType = None, **kwargs, ) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, Line2D]: return gca().psd( @@ -4086,7 +4086,7 @@ def psd( # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.quiver) -def quiver(*args, data=None, **kwargs) -> Quiver: +def quiver(*args, data: DataParamType = None, **kwargs) -> Quiver: __ret = gca().quiver( *args, **({"data": data} if data is not None else {}), **kwargs ) @@ -4120,7 +4120,7 @@ def scatter( edgecolors: Literal["face", "none"] | ColorType | Sequence[ColorType] | None = None, colorizer: Colorizer | None = None, plotnonfinite: bool = False, - data=None, + data: DataParamType = None, **kwargs, ) -> PathCollection: __ret = gca().scatter( @@ -4164,9 +4164,9 @@ def specgram( NFFT: int | None = None, Fs: float | None = None, Fc: int | None = None, - detrend: ( - Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] | None - ) = None, + detrend: Literal["none", "mean", "linear"] + | Callable[[ArrayLike], ArrayLike] + | None = None, window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = None, noverlap: int | None = None, cmap: str | Colormap | None = None, @@ -4179,7 +4179,7 @@ def specgram( vmin: float | None = None, vmax: float | None = None, *, - data=None, + data: DataParamType = None, **kwargs, ) -> tuple[np.ndarray, np.ndarray, np.ndarray, AxesImage]: __ret = gca().specgram( @@ -4255,7 +4255,7 @@ def stem( bottom: float = 0, label: str | None = None, orientation: Literal["vertical", "horizontal"] = "vertical", - data=None, + data: DataParamType = None, ) -> StemContainer: return gca().stem( *args, @@ -4276,7 +4276,7 @@ def step( y: ArrayLike, *args, where: Literal["pre", "post", "mid"] = "pre", - data=None, + data: DataParamType = None, **kwargs, ) -> list[Line2D]: return gca().step( @@ -4480,14 +4480,15 @@ def violinplot( showmedians: bool = False, quantiles: Sequence[float | Sequence[float]] | None = None, points: int = 100, - bw_method: ( - Literal["scott", "silverman"] | float | Callable[[GaussianKDE], float] | None - ) = None, + bw_method: Literal["scott", "silverman"] + | float + | Callable[[GaussianKDE], float] + | None = None, side: Literal["both", "low", "high"] = "both", facecolor: Sequence[ColorType] | ColorType | None = None, linecolor: Sequence[ColorType] | ColorType | None = None, *, - data=None, + data: DataParamType = None, ) -> dict[str, Collection]: return gca().violinplot( dataset, @@ -4518,7 +4519,7 @@ def vlines( linestyles: LineStyleType = "solid", label: str = "", *, - data=None, + data: DataParamType = None, **kwargs, ) -> LineCollection: return gca().vlines( @@ -4543,7 +4544,7 @@ def xcorr( usevlines: bool = True, maxlags: int = 10, *, - data=None, + data: DataParamType = None, **kwargs, ) -> tuple[np.ndarray, np.ndarray, LineCollection | Line2D, Line2D | None]: return gca().xcorr( diff --git a/lib/matplotlib/typing.py b/lib/matplotlib/typing.py index 87016984da12..7fd9bdeefa8f 100644 --- a/lib/matplotlib/typing.py +++ b/lib/matplotlib/typing.py @@ -13,7 +13,7 @@ from collections.abc import Hashable, Sequence import pathlib from typing import Any, Literal, TypeAlias, TypeVar, Union -from collections.abc import Callable +from collections.abc import Callable, Mapping from . import path from ._enums import JoinStyle, CapStyle @@ -22,6 +22,9 @@ from .markers import MarkerStyle from .transforms import Bbox, Transform +DataParamType: TypeAlias = Mapping[str, Any] | None +"""The type of the *data* parameter in plotting functions.""" + RGBColorType: TypeAlias = tuple[float, float, float] | str """Any RGB color specification accepted by Matplotlib.""" From 71facf6ba367854a89d82d601193c683c32f36b7 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:36:08 +0200 Subject: [PATCH 002/215] Run boilerplate with current version of black --- lib/matplotlib/pyplot.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 777bc4877aa9..65b257096cbc 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -3297,8 +3297,9 @@ def cohere( NFFT: int = 256, Fs: float = 2, Fc: int = 0, - detrend: Literal["none", "mean", "linear"] - | Callable[[ArrayLike], ArrayLike] = mlab.detrend_none, + detrend: ( + Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] + ) = mlab.detrend_none, window: Callable[[ArrayLike], ArrayLike] | ArrayLike = mlab.window_hanning, noverlap: int = 0, pad_to: int | None = None, @@ -3355,9 +3356,9 @@ def csd( NFFT: int | None = None, Fs: float | None = None, Fc: int | None = None, - detrend: Literal["none", "mean", "linear"] - | Callable[[ArrayLike], ArrayLike] - | None = None, + detrend: ( + Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] | None + ) = None, window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = None, noverlap: int | None = None, pad_to: int | None = None, @@ -4054,9 +4055,9 @@ def psd( NFFT: int | None = None, Fs: float | None = None, Fc: int | None = None, - detrend: Literal["none", "mean", "linear"] - | Callable[[ArrayLike], ArrayLike] - | None = None, + detrend: ( + Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] | None + ) = None, window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = None, noverlap: int | None = None, pad_to: int | None = None, @@ -4164,9 +4165,9 @@ def specgram( NFFT: int | None = None, Fs: float | None = None, Fc: int | None = None, - detrend: Literal["none", "mean", "linear"] - | Callable[[ArrayLike], ArrayLike] - | None = None, + detrend: ( + Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] | None + ) = None, window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = None, noverlap: int | None = None, cmap: str | Colormap | None = None, @@ -4480,10 +4481,9 @@ def violinplot( showmedians: bool = False, quantiles: Sequence[float | Sequence[float]] | None = None, points: int = 100, - bw_method: Literal["scott", "silverman"] - | float - | Callable[[GaussianKDE], float] - | None = None, + bw_method: ( + Literal["scott", "silverman"] | float | Callable[[GaussianKDE], float] | None + ) = None, side: Literal["both", "low", "high"] = "both", facecolor: Sequence[ColorType] | ColorType | None = None, linecolor: Sequence[ColorType] | ColorType | None = None, From 46ebe0a373c198726407049d2be17be645803983 Mon Sep 17 00:00:00 2001 From: beelauuu Date: Thu, 23 Apr 2026 15:23:43 -0400 Subject: [PATCH 003/215] ft2font null checks added --- src/ft2font.h | 2 +- src/ft2font_wrapper.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ft2font.h b/src/ft2font.h index 0c438d9107de..09e028f3404c 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -41,7 +41,7 @@ inline char const* ft_error_string(FT_Error error) { #undef __FTERRORS_H__ #define FT_ERROR_START_LIST switch (error) { #define FT_ERRORDEF( e, v, s ) case v: return s; -#define FT_ERROR_END_LIST default: return NULL; } +#define FT_ERROR_END_LIST default: return "unknown error"; } #include FT_ERRORS_H } diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index d0df659c5918..771f1db5a191 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -409,9 +409,9 @@ class PyFT2Font final : public FT2Font { std::set::iterator it = family_names.begin(); std::stringstream ss; - ss<<*it; + ss<< (*it ? *it : "unknown family name"); while(++it != family_names.end()){ - ss<<", "<<*it; + ss<<", "<< (*it ? *it : "unknown family name"); } auto text_helpers = py::module_::import("matplotlib._text_helpers"); From 555b572ca4a4d1e1e027222ececf3bccd60c3219 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 24 Apr 2026 14:57:10 -0400 Subject: [PATCH 004/215] LIC: remove carlogo license When we switch to the pydata theme from our own in 905a8d5928059c6b48ac2eb73361f22e4ede9f15 / https://github.com/matplotlib/matplotlib/pull/19731 we dropped the carlogo (a renamed carlito which is non-proprietary version of Calibri) files from our repos but failed to drop the license. --- LICENSE/LICENSE_CARLOGO | 45 ----------------------------------------- doc/project/license.rst | 6 ------ meson.build | 3 +-- 3 files changed, 1 insertion(+), 53 deletions(-) delete mode 100644 LICENSE/LICENSE_CARLOGO diff --git a/LICENSE/LICENSE_CARLOGO b/LICENSE/LICENSE_CARLOGO deleted file mode 100644 index 8c99c656a0f5..000000000000 --- a/LICENSE/LICENSE_CARLOGO +++ /dev/null @@ -1,45 +0,0 @@ -----> we renamed carlito -> carlogo to comply with the terms <---- - -Copyright (c) 2010-2013 by tyPoland Lukasz Dziedzic with Reserved Font Name "Carlito". - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. - -The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the copyright statement(s). - -"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. - -"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. - -5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/doc/project/license.rst b/doc/project/license.rst index 6a34eff4637d..251c29204eb7 100644 --- a/doc/project/license.rst +++ b/doc/project/license.rst @@ -151,12 +151,6 @@ Fonts .. literalinclude:: ../../LICENSE/LICENSE_BAKOMA :language: none -.. dropdown:: Carlogo - :class-container: sdd - - .. literalinclude:: ../../LICENSE/LICENSE_CARLOGO - :language: none - .. dropdown:: Courier 10 :class-container: sdd diff --git a/meson.build b/meson.build index 820335e2c9d8..7d1f3a433fbb 100644 --- a/meson.build +++ b/meson.build @@ -7,7 +7,7 @@ project( '-m', 'setuptools_scm', check: true).stdout().strip(), # qt_editor backend is MIT # ResizeObserver at end of lib/matplotlib/backends/web_backend/js/mpl.js is CC0 - # Carlogo, STIX, Computer Modern, and Last Resort are OFL + # STIX, Computer Modern, and Last Resort are OFL # DejaVu is Bitstream Vera and Public Domain license: 'PSF-2.0 AND MIT AND CC0-1.0 AND OFL-1.1 AND Bitstream-Vera AND Public-Domain', license_files: [ @@ -15,7 +15,6 @@ project( 'extern/agg24-svn/src/copying', 'LICENSE/LICENSE_AMSFONTS', 'LICENSE/LICENSE_BAKOMA', - 'LICENSE/LICENSE_CARLOGO', 'LICENSE/LICENSE_COLORBREWER', 'LICENSE/LICENSE_COURIERTEN', 'LICENSE/LICENSE_FREETYPE', From 05b91794aeed178feefe57bd6a096e3200531348 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 24 Apr 2026 18:51:23 -0400 Subject: [PATCH 005/215] ci: Only run PR triage on main repo (#31565) I keep getting notifications from GitHub that this is failing to run on my fork. --- .github/workflows/triage_board.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/triage_board.yml b/.github/workflows/triage_board.yml index 9888a68b27db..3ef26369afeb 100644 --- a/.github/workflows/triage_board.yml +++ b/.github/workflows/triage_board.yml @@ -10,6 +10,7 @@ permissions: {} jobs: pr-triage: + if: github.repository == 'matplotlib/matplotlib' runs-on: ubuntu-latest steps: - name: Update PR Triage Board From 2638e3e82360b816cb539f4019a25b69fcab1108 Mon Sep 17 00:00:00 2001 From: Hannah Date: Tue, 28 Apr 2026 15:01:58 -0400 Subject: [PATCH 006/215] added unregister to colormap guide --- galleries/users_explain/colors/colormap-manipulation.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/galleries/users_explain/colors/colormap-manipulation.py b/galleries/users_explain/colors/colormap-manipulation.py index 0cd488857257..8b773b4fac42 100644 --- a/galleries/users_explain/colors/colormap-manipulation.py +++ b/galleries/users_explain/colors/colormap-manipulation.py @@ -299,6 +299,11 @@ def plot_linearmap(cdict): plt.show() +# %% +# Colormaps added to the registry can also be deregistered: + +mpl.colormaps.unregister(my_cmap.name) + # %% # # .. admonition:: References From 9127a7083a005ba7c0277c2498ef136c1bf1d634 Mon Sep 17 00:00:00 2001 From: Brian Lau <103338659+beelauuu@users.noreply.github.com> Date: Wed, 29 Apr 2026 03:32:26 -0400 Subject: [PATCH 007/215] FIX: Polar Radial Tick Warnings Labels Bug (#31577) * Fix polar RadialLocator triggering spurious UserWarning in set_ticklabels Extend the FixedLocator isinstance check in Axis.set_ticklabels and _set_formatter to also recognise locators that wrap a FixedLocator via a .base attribute (e.g. RadialLocator used by polar axes). Previously calling set_ticks(ticks, labels) or set_ticklabels(labels) on a polar axis would always hit the else branch and emit a spurious warning because RadialAxis.set_major_locator automatically wraps any locator in a RadialLocator, hiding the inner FixedLocator from the isinstance check. Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/axis.py | 17 ++++++++++++----- lib/matplotlib/tests/test_polar.py | 10 ++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 0ddfee2d537c..48bbac9264ae 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1999,7 +1999,9 @@ def _set_formatter(self, formatter, level): if (isinstance(formatter, mticker.FixedFormatter) and len(formatter.seq) > 0 - and not isinstance(level.locator, mticker.FixedLocator)): + and not isinstance(level.locator, mticker.FixedLocator) + and not (hasattr(level.locator, 'base') and + isinstance(level.locator.base, mticker.FixedLocator))): _api.warn_external('FixedFormatter should only be used together ' 'with FixedLocator') @@ -2142,16 +2144,21 @@ def set_ticklabels(self, labels, *, minor=False, fontdict=None, **kwargs): if not labels: # eg labels=[]: formatter = mticker.NullFormatter() - elif isinstance(locator, mticker.FixedLocator): + elif (isinstance(locator, mticker.FixedLocator) or + (hasattr(locator, 'base') and + isinstance(locator.base, mticker.FixedLocator))): + # Also handles locators that wrap a FixedLocator (e.g. RadialLocator). + fixed_locator = (locator if isinstance(locator, mticker.FixedLocator) + else locator.base) # Passing [] as a list of labels is often used as a way to # remove all tick labels, so only error for > 0 labels - if len(locator.locs) != len(labels) and len(labels) != 0: + if len(fixed_locator.locs) != len(labels) and len(labels) != 0: raise ValueError( "The number of FixedLocator locations" - f" ({len(locator.locs)}), usually from a call to" + f" ({len(fixed_locator.locs)}), usually from a call to" " set_ticks, does not match" f" the number of labels ({len(labels)}).") - tickd = {loc: lab for loc, lab in zip(locator.locs, labels)} + tickd = {loc: lab for loc, lab in zip(fixed_locator.locs, labels)} func = functools.partial(self._format_with_dict, tickd) formatter = mticker.FuncFormatter(func) else: diff --git a/lib/matplotlib/tests/test_polar.py b/lib/matplotlib/tests/test_polar.py index a805fb61d238..6bb534b96f25 100644 --- a/lib/matplotlib/tests/test_polar.py +++ b/lib/matplotlib/tests/test_polar.py @@ -591,3 +591,13 @@ def test_radial_locator_wrapping(): assert ax.yaxis.isDefault_majloc assert isinstance(ax.yaxis.get_major_locator(), RadialLocator) assert isinstance(ax.yaxis.get_major_locator().base, mticker.LogLocator) + + +def test_set_rticks_ticklabels_no_warning(): + # Regression test: RadialLocator wrapping a FixedLocator must not trigger + # the "set_ticklabels() should only be used with a fixed number of ticks" + # UserWarning when set_ticks()/set_rticks() was called first. + + fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) + ax.set_rticks([0, 1, 2, 3]) + ax.yaxis.set_ticklabels(['zero', 'one', 'two', 'three']) From edc2cd0a0addbb02d20d5a4a5e30bbe5d0fcb149 Mon Sep 17 00:00:00 2001 From: beelauuu Date: Wed, 29 Apr 2026 10:36:35 -0400 Subject: [PATCH 008/215] fix --- lib/matplotlib/lines.py | 2 +- lib/matplotlib/tests/test_collections.py | 12 ++++++++++++ pyproject.toml | 2 ++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 69ad36fb768b..9f179e7bfe42 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -36,7 +36,7 @@ def _get_dash_pattern(style): if isinstance(style, str): style = ls_mapper.get(style, style) # un-dashed styles - if style in ['solid', 'None']: + if style in ['solid', 'None', 'none', '', ' ']: offset = 0 dashes = None # dashed styles diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 9c9b4e643014..b88cd3b3b8a3 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -704,6 +704,18 @@ def test_set_wrong_linestyle(): c.set_linestyle('fuzzy') +@pytest.mark.parametrize('backend', ['agg', 'pdf', 'svg', 'ps']) +@pytest.mark.parametrize('ls', ['', ' ', 'none']) +def test_scatter_empty_linestyle(backend, ls): + # Regression Test: Saving to PDF (and other backends) with ls="" + # on a scatter collection used to crash with "zero-size array + # to reduction operation maximum". + plt.switch_backend(backend) + fig, ax = plt.subplots() + ax.scatter([0, 1], [0, 1], ls=ls) + fig.savefig(io.BytesIO()) + + @mpl.style.context('default') def test_capstyle(): col = mcollections.PathCollection([]) diff --git a/pyproject.toml b/pyproject.toml index f3c38512a2c9..d75db711faf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,6 +184,7 @@ extend-exclude = [ "doc/tutorials", "tools/gh_api.py", ] +target-version = "py311" line-length = 88 [tool.ruff.lint] @@ -295,6 +296,7 @@ convention = "numpy" "galleries/users_explain/text/text_props.py" = ["E501"] [tool.mypy] +python_version = "3.11" ignore_missing_imports = true enable_error_code = [ "ignore-without-code", From b8991ce78a9de20670291a4d62d1ee4824c48f93 Mon Sep 17 00:00:00 2001 From: beelauuu Date: Wed, 29 Apr 2026 10:42:42 -0400 Subject: [PATCH 009/215] other backends don't crash --- lib/matplotlib/tests/test_collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index b88cd3b3b8a3..ecababbb8304 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -704,7 +704,7 @@ def test_set_wrong_linestyle(): c.set_linestyle('fuzzy') -@pytest.mark.parametrize('backend', ['agg', 'pdf', 'svg', 'ps']) +@pytest.mark.parametrize('backend', ['agg', 'pdf']) @pytest.mark.parametrize('ls', ['', ' ', 'none']) def test_scatter_empty_linestyle(backend, ls): # Regression Test: Saving to PDF (and other backends) with ls="" From da77088ecae81233ccc68ff0d45ce3a5e1e18850 Mon Sep 17 00:00:00 2001 From: beelauuu Date: Wed, 29 Apr 2026 10:43:20 -0400 Subject: [PATCH 010/215] stupid build --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d75db711faf0..f3c38512a2c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,7 +184,6 @@ extend-exclude = [ "doc/tutorials", "tools/gh_api.py", ] -target-version = "py311" line-length = 88 [tool.ruff.lint] @@ -296,7 +295,6 @@ convention = "numpy" "galleries/users_explain/text/text_props.py" = ["E501"] [tool.mypy] -python_version = "3.11" ignore_missing_imports = true enable_error_code = [ "ignore-without-code", From 99808374bbd16809a13db4ddae0042198f444691 Mon Sep 17 00:00:00 2001 From: Brian Lau <103338659+beelauuu@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:16:56 -0400 Subject: [PATCH 011/215] Update test_scatter_empty_linestyle for PDF backend Refactor test_scatter_empty_linestyle to use 'pdf' backend only. --- lib/matplotlib/tests/test_collections.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index ecababbb8304..45c860fa0075 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -704,13 +704,9 @@ def test_set_wrong_linestyle(): c.set_linestyle('fuzzy') -@pytest.mark.parametrize('backend', ['agg', 'pdf']) @pytest.mark.parametrize('ls', ['', ' ', 'none']) -def test_scatter_empty_linestyle(backend, ls): - # Regression Test: Saving to PDF (and other backends) with ls="" - # on a scatter collection used to crash with "zero-size array - # to reduction operation maximum". - plt.switch_backend(backend) +def test_scatter_empty_linestyle_pdf(ls): + plt.switch_backend('pdf') fig, ax = plt.subplots() ax.scatter([0, 1], [0, 1], ls=ls) fig.savefig(io.BytesIO()) From c23808dc98ea296a90cde57a821164f23891d5aa Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 29 Apr 2026 15:25:17 -0400 Subject: [PATCH 012/215] Expire some missed deprecations from 3.9 --- doc/api/next_api_changes/removals/31588-ES.rst | 18 ++++++++++++++++++ lib/matplotlib/axes/_axes.py | 4 +--- lib/matplotlib/backend_bases.py | 14 -------------- 3 files changed, 19 insertions(+), 17 deletions(-) create mode 100644 doc/api/next_api_changes/removals/31588-ES.rst diff --git a/doc/api/next_api_changes/removals/31588-ES.rst b/doc/api/next_api_changes/removals/31588-ES.rst new file mode 100644 index 000000000000..8709c5a77f5f --- /dev/null +++ b/doc/api/next_api_changes/removals/31588-ES.rst @@ -0,0 +1,18 @@ +``boxplot`` tick labels +^^^^^^^^^^^^^^^^^^^^^^^ + +The parameter *labels* has been removed in favour of *tick_labels* for clarity and +consistency with `~.Axes.bar`. + +Image path semantics of toolmanager-based tools +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Previously, MEP22 ("toolmanager-based") Tools would try to load their icon +(``tool.image``) relative to the current working directory, or, as a fallback, from +Matplotlib's own image directory. Because both approaches are problematic for +third-party tools (the end-user may change the current working directory at any time, +and third-parties cannot add new icons in Matplotlib's image directory), this behavior +has been removed; instead, ``tool.image`` is now interpreted relative to the directory +containing the source file where the ``Tool.image`` class attribute is defined. +(Defining ``tool.image`` as an absolute path also works and is compatible with both the +old and the new semantics.) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 66770e426386..76659ae2c83d 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -4302,7 +4302,6 @@ def apply_mask(arrays, mask): @_api.make_keyword_only("3.10", "notch") @_preprocess_data() - @_api.rename_parameter("3.9", "labels", "tick_labels") def boxplot(self, x, notch=None, sym=None, vert=None, orientation='vertical', whis=None, positions=None, widths=None, patch_artist=None, bootstrap=None, @@ -4444,8 +4443,7 @@ def boxplot(self, x, notch=None, sym=None, vert=None, values. .. versionchanged:: 3.9 - Renamed from *labels*, which is deprecated since 3.9 - and will be removed in 3.11. + Renamed from *labels*, which is also removed in 3.11. manage_ticks : bool, default: True If True, the tick locations and labels will be adjusted to match diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 508b744ca04d..27c3752858a7 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -3516,20 +3516,6 @@ def _get_image_filename(self, tool): for filename in [filename, filename + self._icon_extension]: if os.path.isfile(filename): return os.path.abspath(filename) - for fname in [ # Fallback; once deprecation elapses. - tool.image, - tool.image + self._icon_extension, - cbook._get_data_path("images", tool.image), - cbook._get_data_path("images", tool.image + self._icon_extension), - ]: - if os.path.isfile(fname): - _api.warn_deprecated( - "3.9", message=f"Loading icon {tool.image!r} from the current " - "directory or from Matplotlib's image directory. This behavior " - "is deprecated since %(since)s and will be removed in %(removal)s; " - "Tool.image should be set to a path relative to the Tool's source " - "file, or to an absolute path.") - return os.path.abspath(fname) def trigger_tool(self, name): """ From f6e850d2a26f9ac63e320f2e7ad7f8462ad8584b Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Wed, 29 Apr 2026 21:32:09 +0100 Subject: [PATCH 013/215] ENH: add wedge_labels parameter for pie (#29152) --- .../deprecations/29152_REC.rst | 13 ++ .../next_whats_new/pie_wedge_labels.rst | 26 ++++ galleries/examples/misc/svg_filter_pie.py | 6 +- .../pie_and_polar_charts/bar_of_pie.py | 7 +- .../pie_and_polar_charts/pie_features.py | 70 ++++++----- lib/matplotlib/_api/__init__.pyi | 1 + lib/matplotlib/axes/_axes.py | 85 ++++++++++++-- lib/matplotlib/axes/_axes.pyi | 5 +- lib/matplotlib/pyplot.py | 10 +- lib/matplotlib/tests/test_axes.py | 111 +++++++++++++----- lib/matplotlib/tests/test_container.py | 7 +- tools/boilerplate.py | 2 + 12 files changed, 262 insertions(+), 81 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/29152_REC.rst create mode 100644 doc/release/next_whats_new/pie_wedge_labels.rst diff --git a/doc/api/next_api_changes/deprecations/29152_REC.rst b/doc/api/next_api_changes/deprecations/29152_REC.rst new file mode 100644 index 000000000000..cedc91e81410 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/29152_REC.rst @@ -0,0 +1,13 @@ +``pie`` *labels* and *labeldistance* parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Currently the *labels* parameter of `~.Axes.pie` is used both for annotating the +pie wedges directly, and for automatic legend entries. For consistency +with other plotting methods, in future *labels* will only be used for the legend. + +The *labeldistance* parameter will therefore default to ``None`` from Matplotlib +3.14, when it will also be deprecated and then removed in Matplotlib 3.16. To +preserve the existing behavior for now, set ``labeldistance=1.1``. For the longer +term, to place labels on the wedges use the new *wedge_labels* and +*wedge_label_distance* parameters of `~.Axes.pie` or the `~.Axes.pie_label` method. +Note that `~.Axes.pie_label` allows for more customization of the label positions via +the *rotate* and *alignment* parameters as well as *distance*. diff --git a/doc/release/next_whats_new/pie_wedge_labels.rst b/doc/release/next_whats_new/pie_wedge_labels.rst new file mode 100644 index 000000000000..9c72742e005e --- /dev/null +++ b/doc/release/next_whats_new/pie_wedge_labels.rst @@ -0,0 +1,26 @@ +New *wedge_labels* parameter for pie +------------------------------------ + +`~.Axes.pie` now accepts a *wedge_labels* parameter as a shortcut to the +`~.Axes.pie_label` method. This may be used for simple annotation of the wedges +of the pie chart. It can take + +* a list of strings, similar to the existing *labels* parameter +* a format string similar to the existing *autopct* parameter, except that it + uses the `str.format` method and it can handle absolute values as well as + fractions/percentages + +*wedge_labels* has an accompanying *wedge_label_distance* parameter, to control +the distance of the labels from the center of the pie. + + +.. plot:: + :include-source: true + :alt: Two pie charts. The chart on the left has labels 'foo' and 'bar' outside the wedges. The chart on the right has labels '1' and '2' inside the wedges. + + import matplotlib.pyplot as plt + + fig, (ax1, ax2) = plt.subplots(ncols=2, layout='constrained') + + ax1.pie([1, 2], wedge_labels=['foo', 'bar'], wedge_label_distance=1.1) + ax2.pie([1, 2], wedge_labels='{absval:d}', wedge_label_distance=0.6) diff --git a/galleries/examples/misc/svg_filter_pie.py b/galleries/examples/misc/svg_filter_pie.py index f8ccc5bcb22b..d438fe77b8a6 100644 --- a/galleries/examples/misc/svg_filter_pie.py +++ b/galleries/examples/misc/svg_filter_pie.py @@ -28,11 +28,11 @@ # We want to draw the shadow for each pie, but we will not use "shadow" # option as it doesn't save the references to the shadow patches. -pie = ax.pie(fracs, explode=explode, labels=labels, autopct='%1.1f%%') +pie = ax.pie(fracs, explode=explode, wedge_labels=labels, wedge_label_distance=1.1) -for w in pie.wedges: +for w, label in zip(pie.wedges, labels): # set the id with the label. - w.set_gid(w.get_label()) + w.set_gid(label) # we don't want to draw the edge of the pie w.set_edgecolor("none") diff --git a/galleries/examples/pie_and_polar_charts/bar_of_pie.py b/galleries/examples/pie_and_polar_charts/bar_of_pie.py index 7c703976db2e..6e58bba5209d 100644 --- a/galleries/examples/pie_and_polar_charts/bar_of_pie.py +++ b/galleries/examples/pie_and_polar_charts/bar_of_pie.py @@ -25,8 +25,11 @@ explode = [0.1, 0, 0] # rotate so that first wedge is split by the x-axis angle = -180 * overall_ratios[0] -pie = ax1.pie(overall_ratios, autopct='%1.1f%%', startangle=angle, - labels=labels, explode=explode) +pie = ax1.pie(overall_ratios, startangle=angle, explode=explode) + +# label the wedges with our label strings and the ratios as percentages +ax1.pie_label(pie, labels, distance=1.1) +ax1.pie_label(pie, '{frac:.1%}', distance=0.6) # bar chart parameters age_ratios = [.33, .54, .07, .06] diff --git a/galleries/examples/pie_and_polar_charts/pie_features.py b/galleries/examples/pie_and_polar_charts/pie_features.py index 8510c09f23a5..80b8ade230b2 100644 --- a/galleries/examples/pie_and_polar_charts/pie_features.py +++ b/galleries/examples/pie_and_polar_charts/pie_features.py @@ -15,15 +15,15 @@ # ------------ # # Plot a pie chart of animals and label the slices. To add -# labels, pass a list of labels to the *labels* parameter +# labels, pass a list of labels to the *wedge_labels* parameter. import matplotlib.pyplot as plt labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' -sizes = [15, 30, 45, 10] +sizes = [12, 24, 36, 8] fig, ax = plt.subplots() -ax.pie(sizes, labels=labels) +ax.pie(sizes, wedge_labels=labels) # %% # Each slice of the pie chart is a `.patches.Wedge` object; therefore in @@ -31,16 +31,44 @@ # the *wedgeprops* argument, as demonstrated in # :doc:`/gallery/pie_and_polar_charts/nested_pie`. # +# Set label positions +# ------------------- +# If you want the labels outside the pie, set a *wedge_label_distance* greater than 1. +# This is the distance from the center of the pie as a fraction of its radius. + +fig, ax = plt.subplots() +ax.pie(sizes, wedge_labels=labels, wedge_label_distance=1.1) + +# %% +# # Auto-label slices # ----------------- # -# Pass a function or format string to *autopct* to label slices. +# Pass a format string to *wedge_labels* to label slices with their values... + +fig, ax = plt.subplots() +ax.pie(sizes, wedge_labels='{absval:.1f}') + +# %% +# +# ...or with their percentages... + +fig, ax = plt.subplots() +ax.pie(sizes, wedge_labels='{frac:.1%}') + +# %% +# +# ...or both. fig, ax = plt.subplots() -ax.pie(sizes, labels=labels, autopct='%1.1f%%') +ax.pie(sizes, wedge_labels='{absval:d}\n{frac:.1%}') + +# %% +# +# For more control over labels, or to add multiple sets, see +# :doc:`/gallery/pie_and_polar_charts/pie_label`. # %% -# By default, the label values are obtained from the percent size of the slice. # # Color slices # ------------ @@ -48,8 +76,7 @@ # Pass a list of colors to *colors* to set the color of each slice. fig, ax = plt.subplots() -ax.pie(sizes, labels=labels, - colors=['olivedrab', 'rosybrown', 'gray', 'saddlebrown']) +ax.pie(sizes, colors=['olivedrab', 'rosybrown', 'gray', 'saddlebrown']) # %% # Hatch slices @@ -58,22 +85,9 @@ # Pass a list of hatch patterns to *hatch* to set the pattern of each slice. fig, ax = plt.subplots() -ax.pie(sizes, labels=labels, hatch=['**O', 'oO', 'O.O', '.||.']) - -# %% -# Swap label and autopct text positions -# ------------------------------------- -# Use the *labeldistance* and *pctdistance* parameters to position the *labels* -# and *autopct* text respectively. - -fig, ax = plt.subplots() -ax.pie(sizes, labels=labels, autopct='%1.1f%%', - pctdistance=1.25, labeldistance=.6) +ax.pie(sizes, hatch=['**O', 'oO', 'O.O', '.||.']) # %% -# *labeldistance* and *pctdistance* are ratios of the radius; therefore they -# vary between ``0`` for the center of the pie and ``1`` for the edge of the -# pie, and can be set to greater than ``1`` to place text outside the pie. # # Explode, shade, and rotate slices # --------------------------------- @@ -86,11 +100,10 @@ # # This example orders the slices, separates (explodes) them, and rotates them. -explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') +explode = (0, 0.2, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') fig, ax = plt.subplots() -ax.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%', - shadow=True, startangle=90) +ax.pie(sizes, explode=explode, wedge_labels='{frac:.1%}', shadow=True, startangle=90) plt.show() # %% @@ -107,8 +120,7 @@ fig, ax = plt.subplots() -ax.pie(sizes, labels=labels, autopct='%.0f%%', - textprops={'size': 'small'}, radius=0.5) +ax.pie(sizes, wedge_labels='{frac:.1%}', textprops={'size': 'small'}, radius=0.5) plt.show() # %% @@ -119,8 +131,8 @@ # the `.Shadow` patch. This can be used to modify the default shadow. fig, ax = plt.subplots() -ax.pie(sizes, explode=explode, labels=labels, autopct='%1.1f%%', - shadow={'ox': -0.04, 'edgecolor': 'none', 'shade': 0.9}, startangle=90) +ax.pie(sizes, explode=explode, shadow={'ox': -0.04, 'edgecolor': 'none', 'shade': 0.9}, + startangle=90) plt.show() # %% diff --git a/lib/matplotlib/_api/__init__.pyi b/lib/matplotlib/_api/__init__.pyi index 0bcce210634f..cf24edf65db5 100644 --- a/lib/matplotlib/_api/__init__.pyi +++ b/lib/matplotlib/_api/__init__.pyi @@ -19,6 +19,7 @@ from .deprecation import ( # noqa: F401, re-exported API _T = TypeVar("_T") class _Unset: ... +UNSET = _Unset() class classproperty(Any): def __init__( diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 66770e426386..d5f2ae1168c7 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -41,6 +41,7 @@ BarContainer, ErrorbarContainer, PieContainer, StemContainer) from matplotlib.text import Text from matplotlib.transforms import _ScaledRotation +from matplotlib._api import UNSET as _UNSET _log = logging.getLogger(__name__) @@ -3534,13 +3535,13 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0, self.add_container(stem_container) return stem_container - @_api.make_keyword_only("3.10", "explode") - @_preprocess_data(replace_names=["x", "explode", "labels", "colors"]) - def pie(self, x, explode=None, labels=None, colors=None, - autopct=None, pctdistance=0.6, shadow=False, labeldistance=1.1, - startangle=0, radius=1, counterclock=True, - wedgeprops=None, textprops=None, center=(0, 0), - frame=False, rotatelabels=False, *, normalize=True, hatch=None): + @_preprocess_data(replace_names=["x", "explode", "labels", "colors", + "wedge_labels"]) + def pie(self, x, *, explode=None, labels=None, colors=None, wedge_labels=None, + wedge_label_distance=0.6, autopct=None, pctdistance=0.6, shadow=False, + labeldistance=_UNSET, startangle=0, radius=1, counterclock=True, + wedgeprops=None, textprops=None, center=(0, 0), frame=False, + rotatelabels=False, normalize=True, hatch=None): """ Plot a pie chart. @@ -3560,7 +3561,13 @@ def pie(self, x, explode=None, labels=None, colors=None, of the radius with which to offset each wedge. labels : list, default: None - A sequence of strings providing the labels for each wedge + A sequence of strings providing the legend labels for each wedge. + + .. deprecated:: 3.12 + In future these labels will not appear on the wedges but only + be made available for the legend (see *labeldistance* below). + To place labels on the wedges, use *wedge_labels* or the + `pie_label` method. colors : :mpltype:`color` or list of :mpltype:`color`, default: None A sequence of colors through which the pie chart will cycle. If @@ -3573,12 +3580,35 @@ def pie(self, x, explode=None, labels=None, colors=None, .. versionadded:: 3.7 + wedge_labels : str or list of str, optional + A sequence of strings providing the labels for each wedge, or a format + string with ``absval`` and/or ``frac`` placeholders. For example, to label + each wedge with its value and the percentage in brackets:: + + wedge_labels="{absval:d} ({frac:.0%})" + + For more control or to add multiple sets of labels, use `pie_label` + instead. + + .. versionadded:: 3.12 + + wedge_label_distance : float, default: 0.6 + The radial position of the wedge labels, relative to the pie radius. + Values > 1 are outside the wedge and values < 1 are inside the wedge. + + .. versionadded:: 3.12 + autopct : None or str or callable, default: None If not *None*, *autopct* is a string or function used to label the wedges with their numeric value. The label will be placed inside the wedge. If *autopct* is a format string, the label will be ``fmt % pct``. If *autopct* is a function, then it will be called. + .. admonition:: Discouraged + + Consider using the *wedge_labels* parameter or `pie_label` + method instead. + pctdistance : float, default: 0.6 The relative distance along the radius at which the text generated by *autopct* is drawn. To draw the text outside the pie, @@ -3591,6 +3621,11 @@ def pie(self, x, explode=None, labels=None, colors=None, If set to ``None``, labels are not drawn but are still stored for use in `.legend`. + .. deprecated:: 3.12 + From v3.14 *labeldistance* will default to ``None`` and will + later be removed altogether. Use *wedge_labels* and + *wedge_label_distance* or the `pie_label` method instead. + shadow : bool or dict, default: False If bool, whether to draw a shadow beneath the pie. If dict, draw a shadow passing the properties in the dict to `.Shadow`. @@ -3672,8 +3707,33 @@ def pie(self, x, explode=None, labels=None, colors=None, raise ValueError('Cannot plot an unnormalized pie with sum(x) > 1') else: fracs = x + + if labeldistance is _UNSET: + # NB: when the labeldistance default changes, both labeldistance and + # rotatelabels should be deprecated for removal. + if labels is not None: + msg = ( + "From %(removal)s labeldistance will default to None, so that the " + "strings provided in the labels parameter are only available for " + "the legend. Later labeldistance will be removed completely. To " + "preserve existing behavior for now, pass labeldistance=1.1. " + "Consider using the wedge_labels parameter or the pie_label method " + "instead of the labels parameter." + ) + _api.warn_deprecated("3.12", message=msg) + labeldistance = 1.1 + if labels is None: labels = [''] * len(x) + else: + if wedge_labels is not None and labeldistance is not None: + raise ValueError( + 'wedge_labels is a replacement for labels when annotating the ' + 'wedges, so the two should not be used together unless ' + 'labeldistance is None. To add multiple sets of labels, use the ' + 'pie_label method.' + ) + if explode is None: explode = [0] * len(x) if len(x) != len(labels): @@ -3731,11 +3791,16 @@ def get_next_color(): pc = PieContainer(slices, x, normalize) - if labeldistance is None: + if wedge_labels is not None: + self.pie_label(pc, wedge_labels, distance=wedge_label_distance, + textprops=textprops) + + elif labeldistance is None: # Insert an empty list of texts for backwards compatibility of the # return value. pc.add_texts([]) - else: + + if labeldistance is not None: # Add labels to the wedges. labels_textprops = { 'fontsize': mpl.rcParams['xtick.labelsize'], diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 1c3e1e560d07..b0871167dd75 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -31,6 +31,7 @@ import matplotlib.tri as mtri import matplotlib.table as mtable import matplotlib.stackplot as mstack import matplotlib.streamplot as mstream +from matplotlib._api import _Unset import PIL.Image from collections.abc import Callable, Iterable, Sequence @@ -310,10 +311,12 @@ class Axes(_AxesBase): explode: ArrayLike | None = ..., labels: Sequence[str] | None = ..., colors: ColorType | Sequence[ColorType] | None = ..., + wedge_labels: str | Sequence | None = ..., + wedge_label_distance: float | Sequence = ..., autopct: str | Callable[[float], str] | None = ..., pctdistance: float = ..., shadow: bool = ..., - labeldistance: float | None = ..., + labeldistance: float | None | _Unset = ..., startangle: float = ..., radius: float = ..., counterclock: bool = ..., diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index dd80da45e332..57f5ac08e398 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -60,6 +60,7 @@ import matplotlib import matplotlib.image from matplotlib import _api +from matplotlib._api import UNSET as _UNSET # Re-exported (import x as x) for typing. from matplotlib import get_backend as get_backend, rcParams as rcParams from matplotlib import cm as cm # noqa: F401 @@ -153,6 +154,7 @@ LogLevel ) from matplotlib.widgets import SubplotTool + from matplotlib._api import _Unset _P = ParamSpec('_P') _R = TypeVar('_R') @@ -3963,13 +3965,16 @@ def phase_spectrum( @_copy_docstring_and_deprecators(Axes.pie) def pie( x: ArrayLike, + *, explode: ArrayLike | None = None, labels: Sequence[str] | None = None, colors: ColorType | Sequence[ColorType] | None = None, + wedge_labels: str | Sequence | None = None, + wedge_label_distance: float | Sequence = 0.6, autopct: str | Callable[[float], str] | None = None, pctdistance: float = 0.6, shadow: bool = False, - labeldistance: float | None = 1.1, + labeldistance: float | None | _Unset = _UNSET, startangle: float = 0, radius: float = 1, counterclock: bool = True, @@ -3978,7 +3983,6 @@ def pie( center: tuple[float, float] = (0, 0), frame: bool = False, rotatelabels: bool = False, - *, normalize: bool = True, hatch: str | Sequence[str] | None = None, data=None, @@ -3988,6 +3992,8 @@ def pie( explode=explode, labels=labels, colors=colors, + wedge_labels=wedge_labels, + wedge_label_distance=wedge_label_distance, autopct=autopct, pctdistance=pctdistance, shadow=shadow, diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 6751666360b1..c812a93cbb6f 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -6628,8 +6628,23 @@ def test_pie_default(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') fig1, ax1 = plt.subplots(figsize=(8, 6)) - ax1.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90) + ax1.pie(sizes, explode=explode, wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90) + + +@image_comparison(['pie_default.png'], style='mpl20') +def test_pie_default_legacy(): + # Same as above, but uses labels parameter. Remove after labeldistance + # parameter deprecation expires. + # The slices will be ordered and plotted counter-clockwise. + labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' + sizes = [15, 30, 45, 10] + colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') + fig1, ax1 = plt.subplots(figsize=(8, 6)) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + ax1.pie(sizes, explode=explode, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90) @image_comparison(['pie_linewidth_0.png', 'pie_linewidth_0.png', 'pie_linewidth_0.png'], @@ -6641,27 +6656,30 @@ def test_pie_linewidth_0(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode, wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90, wedgeprops={'linewidth': 0}) # Set aspect ratio to be equal so that pie is drawn as a circle. plt.axis('equal') - # Reuse testcase from above for a labeled data test + # Reuse testcase from above for a labeled data test. Include legend labels + # to smoke test that they are correctly unpacked. data = {"l": labels, "s": sizes, "c": colors, "ex": explode} fig = plt.figure() ax = fig.gca() - ax.pie("s", explode="ex", labels="l", colors="c", + ax.pie("s", explode="ex", wedge_labels="l", colors="c", wedge_label_distance=1.1, autopct='%1.1f%%', shadow=True, startangle=90, - wedgeprops={'linewidth': 0}, data=data) + labels="l", labeldistance=None, wedgeprops={'linewidth': 0}, + data=data) ax.axis('equal') # And again to test the pyplot functions which should also be able to be # called with a data kwarg plt.figure() - plt.pie("s", explode="ex", labels="l", colors="c", + plt.pie("s", explode="ex", wedge_labels="l", colors="c", wedge_label_distance=1.1, autopct='%1.1f%%', shadow=True, startangle=90, - wedgeprops={'linewidth': 0}, data=data) + labels="l", labeldistance=None, wedgeprops={'linewidth': 0}, + data=data) plt.axis('equal') @@ -6674,8 +6692,8 @@ def test_pie_center_radius(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode, wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90, wedgeprops={'linewidth': 0}, center=(1, 2), radius=1.5) plt.annotate("Center point", xy=(1, 2), xytext=(1, 1.3), @@ -6694,8 +6712,8 @@ def test_pie_linewidth_2(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode, wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90, wedgeprops={'linewidth': 2}) # Set aspect ratio to be equal so that pie is drawn as a circle. plt.axis('equal') @@ -6709,8 +6727,8 @@ def test_pie_ccw_true(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode, wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90, counterclock=True) # Set aspect ratio to be equal so that pie is drawn as a circle. plt.axis('equal') @@ -6725,35 +6743,53 @@ def test_pie_frame_grid(): # only "explode" the 2nd slice (i.e. 'Hogs') explode = (0, 0.1, 0, 0) - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode, wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90, wedgeprops={'linewidth': 0}, frame=True, center=(2, 2)) - plt.pie(sizes[::-1], explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes[::-1], explode=explode, wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90, wedgeprops={'linewidth': 0}, frame=True, center=(5, 2)) - plt.pie(sizes, explode=explode[::-1], labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, + plt.pie(sizes, explode=explode[::-1], wedge_labels=labels, wedge_label_distance=1.1, + colors=colors, autopct='%1.1f%%', shadow=True, startangle=90, wedgeprops={'linewidth': 0}, frame=True, center=(3, 5)) # Set aspect ratio to be equal so that pie is drawn as a circle. plt.axis('equal') +@image_comparison(['pie_rotatelabels_true.png'], style='mpl20') +def test_pie_label_rotate(): + # The slices will be ordered and plotted counter-clockwise. + labels = 'Hogwarts', 'Frogs', 'Dogs', 'Logs' + sizes = [15, 30, 45, 10] + colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Frogs') + + pie = plt.pie(sizes, explode=explode, wedge_labels='{frac:.1%}', colors=colors, + shadow=True, startangle=90) + plt.pie_label(pie, labels, distance=1.1, rotate=True) + # Set aspect ratio to be equal so that pie is drawn as a circle. + plt.axis('equal') + + @image_comparison(['pie_rotatelabels_true.png'], style='mpl20') def test_pie_rotatelabels_true(): + # As above but using legacy labels and rotatelabels parameters. Remove + # when the labeldistance parameter deprecation expires. # The slices will be ordered and plotted counter-clockwise. labels = 'Hogwarts', 'Frogs', 'Dogs', 'Logs' sizes = [15, 30, 45, 10] colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] - explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') + explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Frogs') - plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, - rotatelabels=True) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + plt.pie(sizes, explode=explode, labels=labels, colors=colors, + autopct='%1.1f%%', shadow=True, startangle=90, + rotatelabels=True) # Set aspect ratio to be equal so that pie is drawn as a circle. plt.axis('equal') @@ -6765,7 +6801,7 @@ def test_pie_nolabel_but_legend(): colors = ['yellowgreen', 'gold', 'lightskyblue', 'lightcoral'] explode = (0, 0.1, 0, 0) # only "explode" the 2nd slice (i.e. 'Hogs') plt.pie(sizes, explode=explode, labels=labels, colors=colors, - autopct='%1.1f%%', shadow=True, startangle=90, labeldistance=None, + wedge_labels='{frac:.1%}', shadow=True, startangle=90, labeldistance=None, rotatelabels=True) plt.axis('equal') plt.ylim(-1.2, 1.2) @@ -6806,9 +6842,13 @@ def test_pie_textprops(): rotation_mode="anchor", size=12, color="red") - _, texts, autopct = plt.gca().pie(data, labels=labels, autopct='%.2f', - textprops=textprops) - for labels in [texts, autopct]: + fig, ax = plt.subplots() + + pie1 = ax.pie(data, wedge_labels=labels, autopct='%.2f', textprops=textprops) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + pie2 = ax.pie(data, labels=labels, textprops=textprops) + + for labels in pie1.texts + pie2.texts: for tx in labels: assert tx.get_ha() == textprops["horizontalalignment"] assert tx.get_va() == textprops["verticalalignment"] @@ -6836,7 +6876,7 @@ def test_pie_invalid_labels(): # Test ValueError raised when feeding short labels list to axes.pie fig, ax = plt.subplots() with pytest.raises(ValueError): - ax.pie([1, 2, 3], labels=["One", "Two"]) + ax.pie([1, 2, 3], labels=["One", "Two"], labeldistance=None) def test_pie_invalid_radius(): @@ -6846,6 +6886,13 @@ def test_pie_invalid_radius(): ax.pie([1, 2, 3], radius=-5) +def test_pie_wedge_labels_and_labels(): + fig, ax = plt.subplots() + with pytest.raises(ValueError, match='wedge_labels is a replacement for labels'): + ax.pie([1, 2], wedge_labels=['spam', 'eggs'], labels=['bacon', 'beans'], + labeldistance=1.2) + + def test_normalize_kwarg_pie(): fig, ax = plt.subplots() x = [0.3, 0.3, 0.1] @@ -10323,13 +10370,13 @@ def test_pie_non_finite_values(): df = [5, float('nan'), float('inf')] with pytest.raises(ValueError, match='Wedge sizes must be finite numbers'): - ax.pie(df, labels=['A', 'B', 'C']) + ax.pie(df) def test_pie_all_zeros(): fig, ax = plt.subplots() with pytest.raises(ValueError, match="All wedge sizes are zero"): - ax.pie([0, 0], labels=["A", "B"]) + ax.pie([0, 0]) def test_animated_artists_not_drawn_by_default(): diff --git a/lib/matplotlib/tests/test_container.py b/lib/matplotlib/tests/test_container.py index b7dfe1196685..d27ee1115171 100644 --- a/lib/matplotlib/tests/test_container.py +++ b/lib/matplotlib/tests/test_container.py @@ -57,10 +57,13 @@ def test_barcontainer_position_centers__bottoms__tops(): def test_piecontainer_remove(): fig, ax = plt.subplots() - pie = ax.pie([2, 3], labels=['foo', 'bar'], autopct="%1.0f%%") + pie = ax.pie([2, 3], wedge_labels=['foo', 'bar'], autopct="%1.0f%%") ax.pie_label(pie, ['baz', 'qux']) + assert len(ax.patches) == 2 - assert len(ax.texts) == 6 + # We have added 6 labels but pie also adds an empty Text artist to each + # wedge if labeldistance is not None and labels is not passed + assert len(ax.texts) == 8 pie.remove() assert not ax.patches diff --git a/tools/boilerplate.py b/tools/boilerplate.py index 0a1a26c7cb76..c312929b67a2 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -95,6 +95,8 @@ def __init__(self, value): self._repr = "np.mean" elif value is _api.deprecation._deprecated_parameter: self._repr = "_api.deprecation._deprecated_parameter" + elif value is _api.UNSET: + self._repr = "_UNSET" elif isinstance(value, Enum): # Enum str is Class.Name whereas their repr is . self._repr = f'{type(value).__name__}.{value.name}' From de9e18441334c35637f406aeedbb2d56a877dd3a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 29 Apr 2026 15:28:03 -0400 Subject: [PATCH 014/215] Extend invalid hatch pattern deprecation again The comment says it should be 1 release after custom hatches are implemented, and they aren't implemented in 3.11, so this needs to go up. --- lib/matplotlib/hatch.py | 2 +- lib/matplotlib/tests/test_axes.py | 16 ++++------------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/hatch.py b/lib/matplotlib/hatch.py index 5e0b6d761a98..4feb90d34715 100644 --- a/lib/matplotlib/hatch.py +++ b/lib/matplotlib/hatch.py @@ -206,7 +206,7 @@ def _validate_hatch_pattern(hatch): invalids = ''.join(sorted(invalids)) _api.warn_deprecated( '3.4', - removal='3.11', # one release after custom hatches (#20690) + removal='3.13', # one release after custom hatches (#20690) message=f'hatch must consist of a string of "{valid}" or ' 'None, but found the following invalid values ' f'"{invalids}". Passing invalid values is deprecated ' diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 6751666360b1..91836f9455b3 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -10047,21 +10047,13 @@ def assert_not_in_reference_cycle(start): def test_boxplot_tick_labels(): - # Test the renamed `tick_labels` parameter. - # Test for deprecation of old name `labels`. + # Test the `tick_labels` parameter. np.random.seed(19680801) data = np.random.random((10, 3)) - fig, axs = plt.subplots(nrows=1, ncols=2, sharey=True) - # Should get deprecation warning for `labels` - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match='has been renamed \'tick_labels\''): - axs[0].boxplot(data, labels=['A', 'B', 'C']) - assert [l.get_text() for l in axs[0].get_xticklabels()] == ['A', 'B', 'C'] - - # Test the new tick_labels parameter - axs[1].boxplot(data, tick_labels=['A', 'B', 'C']) - assert [l.get_text() for l in axs[1].get_xticklabels()] == ['A', 'B', 'C'] + fig, ax = plt.subplots() + ax.boxplot(data, tick_labels=['A', 'B', 'C']) + assert [l.get_text() for l in ax.get_xticklabels()] == ['A', 'B', 'C'] @needs_usetex From 7814ada10b25ba4a8947957e717a9c0406a98ae8 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:50:53 +0200 Subject: [PATCH 015/215] DOC: Improve testing documentation (#31584) Rordered topics and made descriptions more concise. --------- Co-authored-by: hannah --- doc/devel/testing.rst | 117 ++++++++++++++++++++++++++++++------------ 1 file changed, 83 insertions(+), 34 deletions(-) diff --git a/doc/devel/testing.rst b/doc/devel/testing.rst index cbde2bed7979..0b9390477b10 100644 --- a/doc/devel/testing.rst +++ b/doc/devel/testing.rst @@ -100,34 +100,45 @@ to the folder where the baseline test images are stored. The triage tool require :ref:`QT ` is installed. -Writing a simple test ---------------------- +Writing tests +------------- +Tests are located in :file:`lib/matplotlib/tests`. They are organized to mirror +the structure of the code in :file:`lib/matplotlib`. For example, tests for +the ``mathtext.py`` module are in :file:`lib/matplotlib/tests/test_mathtext.py`. -Many elements of Matplotlib can be tested using standard tests. For -example, here is a test from :file:`matplotlib/tests/test_basic.py`:: +Naming follows standard pytest conventions: - def test_simple(): - """ - very simple example test - """ - assert 1 + 1 == 2 +- files begin with ``"test_"`` +- test functions begin with ``"test_"`` +- test classes begin with ``"Test"``. -Pytest determines which functions are tests by searching for files whose names -begin with ``"test_"`` and then within those files for functions beginning with -``"test"`` or classes beginning with ``"Test"``. +We prefer simple test functions, but test classes are also acceptable. +Test function names should be descriptive of what they are testing, and long names +like ``test_to_rgba_array_accepts_color_alpha_tuple_with_multiple_colors()`` are +perfectly fine. -Some tests have internal side effects that need to be cleaned up after their -execution (such as created figures or modified `.rcParams`). The pytest fixture -``matplotlib.testing.conftest.mpl_test_settings`` will automatically clean -these up; there is no need to do anything further. +Unit tests +^^^^^^^^^^ -Random data in tests --------------------- +Many elements of Matplotlib can be tested using simple unit tests, e.g. :: -Random data is a very convenient way to generate data for examples, -however the randomness is problematic for testing (as the tests -must be deterministic!). To work around this set the seed in each test. -For numpy's default random number generator use:: + def test_to_rgba_explicit_alpha_overrides_tuple_alpha(): + assert mcolors.to_rgba(('red', 0.1), alpha=0.9) == (1, 0, 0, 0.9) + +Data in tests +^^^^^^^^^^^^^ +Try to use minimal explicit data, such as +``[1, 2, 3]``, ``range(5)`` or ``np.arange(5)``, because it +makes the test more readable. + +When you need more and non-trivial data, generate it programmatically, e.g. :: + + x = np.linspace(0, 2*np.pi, 101) + y = 2 * np.sin(x) + 1 + +Use random numbers only when an algorithmic way to generate the data is too +cumbersome or impossible. In this case, set the seed to a fixed value to make +the test deterministic. For numpy's default random number generator use :: import numpy as np rng = np.random.default_rng(19680801) @@ -136,10 +147,56 @@ and then use ``rng`` when generating the random numbers. The seed is :ref:`John Hunter's ` birthday. +Test cleanup +^^^^^^^^^^^^ +We often need to create figures or to modify `.rcParams` to test some functionality. +Cleanup of such side effects is handled automatically through a pytest fixture +(``matplotlib.testing.conftest.mpl_test_settings``) so that no manual cleanup is +necessary. + +In particular, you don't need to call ``plt.close()``. + +Testing with figures and Axes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +When you need figures and/or Axes, create them through the standard methods +(``plt.figure()``, ``plt.subplots()``, etc.). + +Creating figures and Axes is rather expensive (>100ms). Only create as many as you need for +the test, and reuse them if possible. It is perfectly fine to test multiple parametrizations +or related functionality in one test; i.e. extend the classical test structure +*Arrange–Act–Assert* with multiple *Act-Assert* blocks, e.g. :: + + def test_stackplot_facecolor(): + # Test that facecolors are properly passed and take precedence over colors parameter + x = np.linspace(0, 10, 10) + y1 = 1.0 * x + y2 = 2.0 * x + 1 + + fig, ax = plt.subplots() + + facecolors = ['r', 'b'] + + colls = ax.stackplot(x, y1, y2, facecolor=facecolors, colors=['c', 'm']) + for coll, fcolor in zip(colls, facecolors): + assert mcolors.same_color(coll.get_facecolor(), fcolor) + + # Plural alias should also work + colls = ax.stackplot(x, y1, y2, facecolors=facecolors, colors=['c', 'm']) + for coll, fcolor in zip(colls, facecolors): + assert mcolors.same_color(coll.get_facecolor(), fcolor) + +Assert values rather than visual results when feasible. This is clearer, +less computationally expensive and less fragile than comparing images, e.g. :: + + def test_savefig_preserve_layout_engine(): + fig = plt.figure(layout='compressed') + fig.savefig(io.BytesIO(), bbox_inches='tight') + assert fig.get_layout_engine()._compress + .. _image-comparison: -Writing an image comparison test --------------------------------- +Testing with reference images +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Writing an image-based test is only slightly more difficult than a simple test. The main consideration is that you must specify the "baseline", or @@ -180,9 +237,8 @@ texts (labels, tick labels, etc) are not really part of what is tested, use the will lead to smaller figures and reduce possible issues with font mismatch on different platforms. - -Compare two methods of creating an image -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Testing by comparing two methods to create an image +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Baseline images take a lot of space in the Matplotlib repository. An alternative approach for image comparison tests is to use the @@ -228,13 +284,6 @@ See the documentation of `~matplotlib.testing.decorators.image_comparison` and `~matplotlib.testing.decorators.check_figures_equal` for additional information about their use. -Creating a new module in matplotlib.tests ------------------------------------------ - -We try to keep the tests categorized by the primary module they are -testing. For example, the tests related to the ``mathtext.py`` module -are in ``test_mathtext.py``. - Using GitHub Actions for CI --------------------------- From cf25ca50e487012bee9742b2f8b80d357b785073 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 28 Apr 2026 13:46:25 +0200 Subject: [PATCH 016/215] FIX: URL links in SVG should have target='_blank' When embedding SVG in HTML, link `href`s can target different browsing contexts. This e.g. leads to links inside SVGs in #31497 replacing the SVG image instead of opening a new browser window. The solution is to set `target="_blank"` for all links that implement `Artist.get_url()`. This is always intended as an external link, so universally adding `target="_blank"` is justified. Background info: https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/target https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/target#_blank --- doc/api/next_api_changes/behavior/31578-TH.rst | 10 ++++++++++ lib/matplotlib/backends/backend_svg.py | 10 +++++----- lib/matplotlib/tests/test_backend_svg.py | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/31578-TH.rst diff --git a/doc/api/next_api_changes/behavior/31578-TH.rst b/doc/api/next_api_changes/behavior/31578-TH.rst new file mode 100644 index 000000000000..0607652c7c8f --- /dev/null +++ b/doc/api/next_api_changes/behavior/31578-TH.rst @@ -0,0 +1,10 @@ +SVG links open in new tab or window +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +`.Artist.set_url` allows to turn the Artist into a link. In SVG output, +this has been implemented without specifying a `target`_ attribute. The default +target value "_self" resulted in replacing the SVG document with the linked page +when the link was clicked. + +The target is now set to "_blank" so that the link opens in a new tab or window. + +.. _target: https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/target diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 6445915de38b..24790356b9d7 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -697,7 +697,7 @@ def draw_path(self, gc, path, transform, rgbFace=None): sketch=gc.get_sketch_params()) if gc.get_url() is not None: - self.writer.start('a', {'xlink:href': gc.get_url()}) + self.writer.start('a', {'xlink:href': gc.get_url(), 'target': '_blank'}) self.writer.element('path', d=path_data, **self._get_clip_attrs(gc), style=self._get_style(gc, rgbFace)) if gc.get_url() is not None: @@ -730,7 +730,7 @@ def draw_markers( writer.start('g', **self._get_clip_attrs(gc)) if gc.get_url() is not None: - self.writer.start('a', {'xlink:href': gc.get_url()}) + self.writer.start('a', {'xlink:href': gc.get_url(), 'target': '_blank'}) trans_and_flip = self._make_flip_transform(trans) attrib = {'xlink:href': f'#{oid}'} clip = (0, 0, self.width*72, self.height*72) @@ -788,7 +788,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, antialiaseds, urls, offset_position, hatchcolors=hatchcolors): url = gc0.get_url() if url is not None: - writer.start('a', attrib={'xlink:href': url}) + writer.start('a', attrib={'xlink:href': url, 'target': '_blank'}) clip_attrs = self._get_clip_attrs(gc0) if clip_attrs: writer.start('g', **clip_attrs) @@ -966,7 +966,7 @@ def draw_image(self, gc, x, y, im, transform=None): url = gc.get_url() if url is not None: - self.writer.start('a', attrib={'xlink:href': url}) + self.writer.start('a', attrib={'xlink:href': url, 'target': '_blank'}) attrib = {} oid = gc.get_gid() @@ -1288,7 +1288,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): self.writer.start('g', **clip_attrs) if gc.get_url() is not None: - self.writer.start('a', {'xlink:href': gc.get_url()}) + self.writer.start('a', {'xlink:href': gc.get_url(), 'target': '_blank'}) if mpl.rcParams['svg.fonttype'] == 'path': self._draw_text_as_path(gc, x, y, s, prop, angle, ismath, mtext) diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index ba565eadb01b..6b63990f7620 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -65,7 +65,7 @@ def test_text_urls(): fig.savefig(fd, format='svg') buf = fd.getvalue().decode() - expected = f'' + expected = f'' assert expected in buf From f94bee58fd9c4128607df4a9f0baf5513d388f85 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 1 May 2026 11:44:52 +0200 Subject: [PATCH 017/215] DOC: Explain how to selectively restore ticks that are removed by sharex Closes #24958. --- .../subplots_axes_and_figures/subplots_demo.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/galleries/examples/subplots_axes_and_figures/subplots_demo.py b/galleries/examples/subplots_axes_and_figures/subplots_demo.py index 0e3cb1102230..0477a15d49d9 100644 --- a/galleries/examples/subplots_axes_and_figures/subplots_demo.py +++ b/galleries/examples/subplots_axes_and_figures/subplots_demo.py @@ -141,8 +141,17 @@ # %% # For subplots that are sharing axes one set of tick labels is enough. Tick # labels of inner Axes are automatically removed by *sharex* and *sharey*. -# Still there remains an unused empty space between the subplots. -# +# You can selectively restore them using `~.axes.Axes.tick_params`. + +fig, axs = plt.subplots(3, sharex=True, sharey=True) +fig.suptitle('Restored xtick labels on to Axes') +axs[0].plot(x, y ** 2) +axs[1].plot(x, 0.3 * y, 'o') +axs[2].plot(x, y, '+') + +axs[0].tick_params(labelbottom=True) + +# It is also possible to remove the empty space between the subplots. # To precisely control the positioning of the subplots, one can explicitly # create a `.GridSpec` with `.Figure.add_gridspec`, and then call its # `~.GridSpecBase.subplots` method. For example, we can reduce the height From 2c41a2dee2945dea7826c20287a0ecfe9202dc8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Melissa=20Weber=20Mendon=C3=A7a?= Date: Fri, 1 May 2026 15:06:00 -0300 Subject: [PATCH 018/215] DOC, DX: Add note about stepping back from PR reviews --- doc/devel/pr_guide.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/doc/devel/pr_guide.rst b/doc/devel/pr_guide.rst index f29475cbf8d5..8cf31db0254b 100644 --- a/doc/devel/pr_guide.rst +++ b/doc/devel/pr_guide.rst @@ -208,12 +208,21 @@ Review push changes to the contributor branch, or merge the PR and then open a new PR against upstream. -* If you push to a contributor branch leave a comment explaining what +* If you push to a contributor branch, leave a comment explaining what you did, ex "I took the liberty of pushing a small clean-up PR to your branch, thanks for your work.". If you are going to make substantial changes to the code or intent of the PR please check with the contributor first. +* If you find yourself spending too much time on a PR, or feeling frustrated, + it's ok to step back. You can ask for help from other reviewers, or if you are + the only reviewer, you can ask the contributor to find another reviewer or to + wait until you have more time. Make sure to communicate with the contributor + to set the right expectations, e.g. "I currently don't have the bandwidth to + review this PR, but will try to loop someone else in." If you feel like this + PR is not a good fit for the project, you can close it with an explanation or + add the "status: autoclose candidate" label to trigger the autoclose workflow. + .. _pr-approval: Approval From 4ff533c88350b7bcc11f57e9a91aca32957a7256 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 1 May 2026 21:16:32 +0200 Subject: [PATCH 019/215] Update galleries/examples/subplots_axes_and_figures/subplots_demo.py Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --- galleries/examples/subplots_axes_and_figures/subplots_demo.py | 1 + 1 file changed, 1 insertion(+) diff --git a/galleries/examples/subplots_axes_and_figures/subplots_demo.py b/galleries/examples/subplots_axes_and_figures/subplots_demo.py index 0477a15d49d9..87f5ed6a3d34 100644 --- a/galleries/examples/subplots_axes_and_figures/subplots_demo.py +++ b/galleries/examples/subplots_axes_and_figures/subplots_demo.py @@ -151,6 +151,7 @@ axs[0].tick_params(labelbottom=True) +# %% # It is also possible to remove the empty space between the subplots. # To precisely control the positioning of the subplots, one can explicitly # create a `.GridSpec` with `.Figure.add_gridspec`, and then call its From 409ef90ab423956d1cb42e83dbf41b77d3f80583 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 22:44:58 +0000 Subject: [PATCH 020/215] Bump the actions group with 2 updates Bumps the actions group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [j178/prek-action](https://github.com/j178/prek-action). Updates `github/codeql-action` from 4.35.2 to 4.35.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/95e58e9a2cdfd71adc6e0353d5c52f41a045d225...e46ed2cbd01164d986452f91f178727624ae40d7) Updates `j178/prek-action` from 2.0.2 to 2.0.3 - [Release notes](https://github.com/j178/prek-action/releases) - [Commits](https://github.com/j178/prek-action/compare/cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3...6ad80277337ad479fe43bd70701c3f7f8aa74db3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.35.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: j178/prek-action dependency-version: 2.0.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/linting.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9a7b40ccac10..9ee6ac545967 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 with: languages: ${{ matrix.language }} @@ -45,4 +45,4 @@ jobs: pip install --user -v . - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2 + uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index f6af70b8b233..0d6e71198817 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: "3.x" - - uses: j178/prek-action@cbc2f23eb5539cf20d82d1aabd0d0ecbcc56f4e3 # v2.0.2 + - uses: j178/prek-action@6ad80277337ad479fe43bd70701c3f7f8aa74db3 # v2.0.3 with: extra-args: --hook-stage manual --all-files From dcd4f31ad41a9235cbc3a341e28dbc7aed601a9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 1 May 2026 22:52:07 +0000 Subject: [PATCH 021/215] Bump https://github.com/astral-sh/ruff-pre-commit Bumps [https://github.com/astral-sh/ruff-pre-commit](https://github.com/astral-sh/ruff-pre-commit) from v0.15.11 to 0.15.12. This release includes the previously tagged commit. - [Release notes](https://github.com/astral-sh/ruff-pre-commit/releases) - [Commits](https://github.com/astral-sh/ruff-pre-commit/compare/d1b833175a5d08a925900115526febd8fe71c98e...6fec9b7edb08fd9989088709d864a7826dc74e80) --- updated-dependencies: - dependency-name: https://github.com/astral-sh/ruff-pre-commit dependency-version: 0.15.12 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bfce3c54a030..7de40cf539ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: pass_filenames: false - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: d1b833175a5d08a925900115526febd8fe71c98e # frozen: v0.15.11 + rev: 6fec9b7edb08fd9989088709d864a7826dc74e80 # frozen: v0.15.12 hooks: # Run the linter. - id: ruff-check From 6441aff0cba368fc425c8583c5a90595b39ea59b Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 1 May 2026 08:58:20 +0200 Subject: [PATCH 022/215] DOC: Improve docs on writing documentation --- doc/devel/document.rst | 104 +++++++++++++++++++---------------------- 1 file changed, 49 insertions(+), 55 deletions(-) diff --git a/doc/devel/document.rst b/doc/devel/document.rst index bea4771af383..a4a4926fdd95 100644 --- a/doc/devel/document.rst +++ b/doc/devel/document.rst @@ -154,8 +154,8 @@ for opening them in your default browser is: .. _writing-rest-pages: -Write ReST pages -================ +reStructuredText pages +====================== Most documentation is either in the docstrings of individual classes and methods, in explicit ``.rst`` files, or in examples and tutorials. @@ -243,11 +243,15 @@ nor the ````literal```` role: Do not describe ``argument`` like this. -Write mathematical expressions ------------------------------- +Mathematical expressions +------------------------ +Use sphinx's built in math support: + +- **Inline math:** Use the ``:math:`` + `role `__ +- **Math blocks:** Use the ``.. math::`` + `directive `__ -In most cases, you will likely want to use one of `Sphinx's builtin Math -extensions `__. In rare cases we want the rendering of the mathematical text in the documentation html to exactly match with the rendering of the mathematical expression in the Matplotlib figure. In these cases, you can use the @@ -257,17 +261,17 @@ expression in the Matplotlib figure. In these cases, you can use the .. _internal-section-refs: -Refer to other documents and sections -------------------------------------- +Cross-references +---------------- Sphinx_ supports internal references_: -========== =============== =========================================== -Role Links target Representation in rendered HTML -========== =============== =========================================== -|doc-dir|_ document link to a page -|ref-dir|_ reference label link to an anchor associated with a heading -========== =============== =========================================== +========== ============================== =========================================== +Role Link target Representation in rendered HTML +========== ============================== =========================================== +|doc-dir|_ :ref:`page ` link to a page +|ref-dir|_ :ref:`section ` link to an anchor associated with a heading +========== ============================== =========================================== .. The following is a hack to have a link with literal formatting See https://stackoverflow.com/a/4836544 @@ -277,63 +281,53 @@ Role Links target Representation in rendered HTML .. |ref-dir| replace:: ``:ref:`` .. _ref-dir: https://www.sphinx-doc.org/en/master/usage/restructuredtext/roles.html#role-ref -Examples: +.. _link-pages: -.. code-block:: rst +Link to pages +^^^^^^^^^^^^^ - See the :doc:`/install/index` +To cross-link to another page, use the ``:doc:`` role. We generally prefer +absolute paths, starting with ``/`` as the :file:`doc` root directory. + +Example: - See the tutorial :ref:`quick_start` +.. code-block:: rst - See the example :doc:`/gallery/lines_bars_and_markers/simple_plot` + See the :doc:`/install/index` will render as: See the :doc:`/install/index` - See the tutorial :ref:`quick_start` - - See the example :doc:`/gallery/lines_bars_and_markers/simple_plot` +.. _link-sections: -Sections can also be given reference labels. For instance from the -:doc:`/install/index` link: - -.. code-block:: rst - - .. _clean-install: - - How to completely remove Matplotlib - =================================== +Link to sections +^^^^^^^^^^^^^^^^ - Occasionally, problems with Matplotlib can be solved with a clean... +Use hyphen-separated, descriptive names for reference labels. +Do not encode the documentation hierarchy in the label as that may change; +e.g. do not prefix all *User guide* labels with ``user-``. -and refer to it using the standard reference syntax: +To cross-link a specific section, add a reference label ``.. _label-name:`` +before the section .. code-block:: rst - See :ref:`clean-install` + .. _pr-author-guidelines: -will give the following link: :ref:`clean-install` + Summary for pull request authors + ================================ -To maximize internal consistency in section labeling and references, -use hyphen separated, descriptive labels for section references. -Keep in mind that contents may be reorganized later, so -avoid top level names in references like ``user`` or ``devel`` -or ``faq`` unless necessary, because for example the FAQ "what is a -backend?" could later become part of the users guide, so the label: +and then link to with ``:ref:`label-name``` .. code-block:: rst - .. _what-is-a-backend: - -is better than: + See the :ref:`pr-author-guidelines` -.. code-block:: rst +This will render as: - .. _faq-backend: + See the :ref:`pr-author-guidelines` -In addition, since underscores are widely used by Sphinx itself, use -hyphens to separate words. .. _referring-to-other-code: @@ -461,8 +455,8 @@ For clarity, do not use relative links. .. _writing-docstrings: -Write API documentation -======================= +API documentation +================= The API reference documentation describes the library interfaces, e.g. inputs, outputs, and expected behavior. Most of the API documentation is written in docstrings. These are @@ -957,8 +951,8 @@ Example: .. _writing-examples-and-tutorials: -Write examples and tutorials -============================ +Examples and tutorials +====================== Examples and tutorials are Python scripts that are run by `Sphinx Gallery`_. Sphinx Gallery finds ``*.py`` files in source directories and runs the files to @@ -1226,10 +1220,10 @@ Format :code: The code should be about 5-10 lines with minimal customization. Plots in this gallery use the ``_mpl-gallery`` stylesheet for a uniform aesthetic. -Analytics -========== +Website analytics +================= -Documentation page analytics are available at +Analytics of our hosted documentation https://matplotlib.org is available at https://views.scientific-python.org/matplotlib.org. From c19bfe85534b7524b682316e1ea5d3b03b5d2d67 Mon Sep 17 00:00:00 2001 From: beelauuu Date: Sat, 2 May 2026 12:28:58 -0400 Subject: [PATCH 023/215] comment --- lib/matplotlib/tests/test_collections.py | 3 +++ pyproject.toml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 45c860fa0075..46122b8b1e6a 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -706,6 +706,9 @@ def test_set_wrong_linestyle(): @pytest.mark.parametrize('ls', ['', ' ', 'none']) def test_scatter_empty_linestyle_pdf(ls): + # Regression test: '', ' ', and 'none' are documented "draw nothing" + # linestyle aliases but were not recognized by _get_dash_pattern, causing + # savefig to PDF to crash with "zero-size array to reduction operation maximum". plt.switch_backend('pdf') fig, ax = plt.subplots() ax.scatter([0, 1], [0, 1], ls=ls) diff --git a/pyproject.toml b/pyproject.toml index f3c38512a2c9..d75db711faf0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,6 +184,7 @@ extend-exclude = [ "doc/tutorials", "tools/gh_api.py", ] +target-version = "py311" line-length = 88 [tool.ruff.lint] @@ -295,6 +296,7 @@ convention = "numpy" "galleries/users_explain/text/text_props.py" = ["E501"] [tool.mypy] +python_version = "3.11" ignore_missing_imports = true enable_error_code = [ "ignore-without-code", From f45ed51e696fd4ffa99730e15517b14f93674483 Mon Sep 17 00:00:00 2001 From: beelauuu Date: Sat, 2 May 2026 12:30:50 -0400 Subject: [PATCH 024/215] build --- pyproject.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d75db711faf0..f3c38512a2c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -184,7 +184,6 @@ extend-exclude = [ "doc/tutorials", "tools/gh_api.py", ] -target-version = "py311" line-length = 88 [tool.ruff.lint] @@ -296,7 +295,6 @@ convention = "numpy" "galleries/users_explain/text/text_props.py" = ["E501"] [tool.mypy] -python_version = "3.11" ignore_missing_imports = true enable_error_code = [ "ignore-without-code", From 83088b8a64c40110bedbc5881461f022efa5f7cc Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 4 May 2026 23:12:13 +0200 Subject: [PATCH 025/215] Remove outdated comment re: implementation of hinting_factor. (#31608) --- src/ft2font.cpp | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index e99f9a7e1095..e853346bf1f4 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -16,31 +16,6 @@ #define M_PI 3.14159265358979323846264338328 #endif -/** - To improve the hinting of the fonts, this code uses a hack - presented here: - - http://agg.sourceforge.net/antigrain.com/research/font_rasterization/index.html - - The idea is to limit the effect of hinting in the x-direction, while - preserving hinting in the y-direction. Since freetype does not - support this directly, the dpi in the x-direction is set higher than - in the y-direction, which affects the hinting grid. Then, a global - transform is placed on the font to shrink it back to the desired - size. While it is a bit surprising that the dpi setting affects - hinting, whereas the global transform does not, this is documented - behavior of FreeType, and therefore hopefully unlikely to change. - The FreeType 2 tutorial says: - - NOTE: The transformation is applied to every glyph that is - loaded through FT_Load_Glyph and is completely independent of - any hinting process. This means that you won't get the same - results if you load a glyph at the size of 24 pixels, or a glyph - at the size at 12 pixels scaled by 2 through a transform, - because the hints will have been computed differently (except - you have disabled hints). - */ - FT_Library _ft2Library; FT2Image::FT2Image(unsigned long width, unsigned long height) From f4cc437d1bd72ec72cfc2a0e318c867c6969392f Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 5 May 2026 05:26:21 +0200 Subject: [PATCH 026/215] DOC: Consolidate shared axis examples (#31605) * DOC: Consolidate shared axis examples This replaces the two existing examples into one single example, that shows the basic concept of sharing and links out for further details. For now, the link goes to the subplots_demo. The sharing part should be refactored out into a tutorial. But that's a separate topic, and the link anchor will move along in such a refactoring. --------- Co-authored-by: hannah --- .../share_axis_lims_views.py | 32 ---------- .../shared_axis_demo.py | 64 ++++++++----------- .../subplots_demo.py | 2 + 3 files changed, 30 insertions(+), 68 deletions(-) delete mode 100644 galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py diff --git a/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py b/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py deleted file mode 100644 index e0aa04d13def..000000000000 --- a/galleries/examples/subplots_axes_and_figures/share_axis_lims_views.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -=========================== -Share axis limits and views -=========================== - -It's common to make two or more plots which share an axis, e.g., two subplots -with time as a common axis. When you pan and zoom around on one, you want the -other to move around with you. To facilitate this, matplotlib Axes support a -``sharex`` and ``sharey`` attribute. When you create a `~.pyplot.subplot` or -`~.pyplot.axes`, you can pass in a keyword indicating what Axes you want to -share with. -""" - -import matplotlib.pyplot as plt -import numpy as np - -t = np.arange(0, 10, 0.01) - -ax1 = plt.subplot(211) -ax1.plot(t, np.sin(2*np.pi*t)) - -ax2 = plt.subplot(212, sharex=ax1) -ax2.plot(t, np.sin(4*np.pi*t)) - -plt.show() - -# %% -# .. tags:: -# -# component: axis -# plot-type: line -# level: beginner diff --git a/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py b/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py index 848db115456a..e5926e8f7ff4 100644 --- a/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py +++ b/galleries/examples/subplots_axes_and_figures/shared_axis_demo.py @@ -3,46 +3,38 @@ Shared axis =========== -You can share the x- or y-axis limits for one axis with another by -passing an `~.axes.Axes` instance as a *sharex* or *sharey* keyword argument. - -Changing the axis limits on one Axes will be reflected automatically -in the other, and vice-versa, so when you navigate with the toolbar -the Axes will follow each other on their shared axis. Ditto for -changes in the axis scaling (e.g., log vs. linear). However, it is -possible to have differences in tick labeling, e.g., you can selectively -turn off the tick labels on one Axes. - -The example below shows how to customize the tick labels on the -various axes. Shared axes share the tick locator, tick formatter, -view limits, and transformation (e.g., log, linear). But the tick labels -themselves do not share properties. This is a feature and not a bug, -because you may want to make the tick labels smaller on the upper -axes, e.g., in the example below. +Use axis sharing when you want to compare data across multiple subplots, and want to +ensure they are on the same scale. To do so, pass ``sharex=True`` and/or ``sharey=True`` +to `~.pyplot.subplots`. + +This ensures the x- or y-axis limits are synchronized across the subplots. Autoscaling +considers the data on all Axes; therefore, any limit changes, including interactive zoom +and pan, will affect all shared axes. + +The plot below illustrates this by showing two different time-series and using *sharex* +to ensure the times are aligned. + +For more info see :ref:`sharing-axes`. + +.. redirect-from:: /gallery/subplots_axes_and_figures/share_axis_lims_views """ import matplotlib.pyplot as plt import numpy as np -t = np.arange(0.01, 5.0, 0.01) -s1 = np.sin(2 * np.pi * t) -s2 = np.exp(-t) -s3 = np.sin(4 * np.pi * t) - -ax1 = plt.subplot(311) -plt.plot(t, s1) -# reduce the fontsize of the tick labels -plt.tick_params('x', labelsize=6) - -# share x only -ax2 = plt.subplot(312, sharex=ax1) -plt.plot(t, s2) -# make these tick labels invisible -plt.tick_params('x', labelbottom=False) - -# share x and y -ax3 = plt.subplot(313, sharex=ax1, sharey=ax1) -plt.plot(t, s3) -plt.xlim(0.01, 5.0) +t1 = np.linspace(0, 8, 201) +y1 = np.sin(2 * np.pi * t1) +t2 = np.linspace(2, 10, 201) +y2 = 20 * np.cos(2 * np.pi * t2)**2 * np.exp(-0.3*t2) + +fig, (ax1, ax2) = plt.subplots(2, sharex=True) + +ax1.plot(t1, y1) +ax1.set_ylabel("Signal 1") + +ax2.plot(t2, y2) +ax2.set_ylabel("Signal 2") +ax2.set_xlabel("Time (s)") + plt.show() # %% diff --git a/galleries/examples/subplots_axes_and_figures/subplots_demo.py b/galleries/examples/subplots_axes_and_figures/subplots_demo.py index 87f5ed6a3d34..ea38a2483fdd 100644 --- a/galleries/examples/subplots_axes_and_figures/subplots_demo.py +++ b/galleries/examples/subplots_axes_and_figures/subplots_demo.py @@ -108,6 +108,8 @@ ax.label_outer() # %% +# .. _sharing-axes: +# # Sharing axes # """""""""""" # From 488d435233a24cb6c78777355303971e7af5a6b7 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 5 May 2026 21:53:44 -0400 Subject: [PATCH 027/215] Make Scale axis parameter handling more flexible Instead of checking the types ourselves, try to bind the old signature, and if possible, use it. Otherwise, try to use the new signature. This is similar to `_api.select_matching_signature` without needing to write a callable for each signature. --- lib/matplotlib/scale.py | 21 ++++++++++---- lib/matplotlib/tests/test_scale.py | 44 ++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 0793bb31e566..a4cce23562d3 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -138,7 +138,7 @@ def _make_axis_parameter_optional(init_func): This decorator ensures backward compatibility for scale classes that previously required an *axis* parameter. It allows constructors to be - callerd with or without the *axis* parameter. + called with or without the *axis* parameter. For simplicity, this does not handle the case when *axis* is passed as a keyword. However, @@ -170,12 +170,16 @@ def _make_axis_parameter_optional(init_func): """ @wraps(init_func) def wrapper(self, *args, **kwargs): - if args and isinstance(args[0], mpl.axis.Axis): - return init_func(self, *args, **kwargs) + sig = inspect.signature(init_func) + try: + # Try old signature. + sig.bind(self, *args, **kwargs) + except TypeError: + # Use the new signature and pass in an unused axis=None. + init_func(self, None, *args, **kwargs) else: - # Remove 'axis' from kwargs to avoid double assignment - axis = kwargs.pop('axis', None) - return init_func(self, axis, *args, **kwargs) + # Use the old signature. + init_func(self, *args, **kwargs) return wrapper @@ -449,6 +453,11 @@ def __init__(self, axis, functions, base=10): ---------- axis : `~matplotlib.axis.Axis` The axis for the scale. + + .. note:: + This parameter is unused and about to be removed in the future. + It can already now be left out because of special preprocessing, + so that ``FuncScaleLog(functions=(forward, inverse))`` is valid. functions : (callable, callable) two-tuple of the forward and inverse functions for the scale. The forward function must be monotonic. diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index 95601ce97b65..104c87adab7b 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -3,6 +3,7 @@ import matplotlib.pyplot as plt from matplotlib.scale import ( AsinhScale, AsinhTransform, + FuncScale, LogitScale, LogTransform, InvertedLogTransform, SymmetricalLogTransform) import matplotlib.scale as mscale @@ -19,6 +20,49 @@ import pytest +def test_optional_axis_signature(): + # There are three types of original signatures possible, and this only tests one + # example class of each: + # 1. `axis` without default: LinearScale, FuncScale, FuncScaleLog + # 2. `axis` with default and more positional parameters: LogitScale + # 3. `axis` with default and only keyword-only parameters: LogScale, AsinhScale, + # SymmetricalLogScale + # Testing with None is sufficient as detection is purely based on the + # signature structure; no type information is involved. + axis = None + + # Old signature with axis positionally. + FuncScale(axis, (lambda x: x, lambda x: x)) + FuncScale(axis, functions=(lambda x: x, lambda x: x)) + LogitScale(axis) + LogitScale(axis, 'clip') + LogitScale(axis, nonpositive='clip') + LogitScale(axis, use_overline=True) + AsinhScale(axis) + AsinhScale(axis, linear_width=2) + AsinhScale(axis, base=3) + AsinhScale(axis, subs=[2, 6]) + # Old signature with axis as keyword. + FuncScale(axis=axis, functions=(lambda x: x, lambda x: x)) + LogitScale(axis=axis) + LogitScale(axis=axis, nonpositive='clip') + LogitScale(axis=axis, use_overline=True) + AsinhScale(axis=axis) + AsinhScale(axis=axis, linear_width=2) + AsinhScale(axis=axis, base=3) + AsinhScale(axis=axis, subs=[2, 6]) + # New signature without axis. + FuncScale((lambda x: x, lambda x: x)) + FuncScale(functions=(lambda x: x, lambda x: x)) + LogitScale() + LogitScale(nonpositive='clip') + LogitScale(use_overline=True) + AsinhScale() + AsinhScale(linear_width=2) + AsinhScale(base=3) + AsinhScale(subs=[2, 6]) + + @check_figures_equal() def test_log_scales(fig_test, fig_ref): ax_test = fig_test.add_subplot(122, yscale='log', xscale='symlog') From b1dfa09c42c0d84fab5b1f9cb2c1051a7a70c5ee Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 6 May 2026 17:11:14 -0400 Subject: [PATCH 028/215] DOC: Inline ScalarMappable reStructuredText entries There is nothing that is dynamic here, so there's no need to generate this with code. --- doc/api/.gitignore | 1 - doc/api/cm_api.rst | 18 +++++++++++++++- doc/conf.py | 53 ---------------------------------------------- 3 files changed, 17 insertions(+), 55 deletions(-) delete mode 100644 doc/api/.gitignore diff --git a/doc/api/.gitignore b/doc/api/.gitignore deleted file mode 100644 index dbed88d89836..000000000000 --- a/doc/api/.gitignore +++ /dev/null @@ -1 +0,0 @@ -scalarmappable.gen_rst diff --git a/doc/api/cm_api.rst b/doc/api/cm_api.rst index c9509389a2bb..8476ab14cb86 100644 --- a/doc/api/cm_api.rst +++ b/doc/api/cm_api.rst @@ -7,4 +7,20 @@ :undoc-members: :show-inheritance: -.. include:: scalarmappable.gen_rst +.. class:: ScalarMappable(colorizer, **kwargs) + :canonical: matplotlib.colorizer._ScalarMappable + + .. automethod:: autoscale + .. automethod:: autoscale_None + .. automethod:: changed + .. autoproperty:: colorbar + .. automethod:: get_alpha + .. automethod:: get_array + .. automethod:: get_clim + .. automethod:: get_cmap + .. autoproperty:: norm + .. automethod:: set_array + .. automethod:: set_clim + .. automethod:: set_cmap + .. automethod:: set_norm + .. automethod:: to_rgba diff --git a/doc/conf.py b/doc/conf.py index 4be3fcf3afee..6651383fcacb 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -837,58 +837,6 @@ def linkcode_resolve(domain, info): extensions.append('sphinx.ext.viewcode') -def generate_ScalarMappable_docs(): - - import matplotlib.colorizer - from numpydoc.docscrape_sphinx import get_doc_object - from pathlib import Path - import textwrap - from sphinx.util.inspect import stringify_signature - target_file = Path(__file__).parent / 'api' / 'scalarmappable.gen_rst' - with open(target_file, 'w') as fout: - fout.write(""" -.. class:: ScalarMappable(colorizer, **kwargs) - :canonical: matplotlib.colorizer._ScalarMappable - -""") - for meth in [ - matplotlib.colorizer._ScalarMappable.autoscale, - matplotlib.colorizer._ScalarMappable.autoscale_None, - matplotlib.colorizer._ScalarMappable.changed, - """ - .. attribute:: colorbar - - The last colorbar associated with this ScalarMappable. May be None. -""", - matplotlib.colorizer._ScalarMappable.get_alpha, - matplotlib.colorizer._ScalarMappable.get_array, - matplotlib.colorizer._ScalarMappable.get_clim, - matplotlib.colorizer._ScalarMappable.get_cmap, - """ - .. property:: norm -""", - matplotlib.colorizer._ScalarMappable.set_array, - matplotlib.colorizer._ScalarMappable.set_clim, - matplotlib.colorizer._ScalarMappable.set_cmap, - matplotlib.colorizer._ScalarMappable.set_norm, - matplotlib.colorizer._ScalarMappable.to_rgba, - ]: - if isinstance(meth, str): - fout.write(meth) - else: - name = meth.__name__ - sig = stringify_signature(inspect.signature(meth)) - docstring = textwrap.indent( - str(get_doc_object(meth)), - ' ' - ).rstrip() - fout.write(f""" - .. method:: {name}{sig} -{docstring} - -""") - - # ----------------------------------------------------------------------------- # Sphinx setup # ----------------------------------------------------------------------------- @@ -902,5 +850,4 @@ def setup(app): app.connect('autodoc-process-bases', autodoc_process_bases) if sphinx.version_info[:2] < (7, 1): app.connect('html-page-context', add_html_cache_busting, priority=1000) - generate_ScalarMappable_docs() app.config.autodoc_use_legacy_class_based = True From 85641970d44b4e54a9154d1931bf39b2c5a2c910 Mon Sep 17 00:00:00 2001 From: Ricardo Peres <20727577+ricmperes@users.noreply.github.com> Date: Thu, 7 May 2026 10:12:53 +0100 Subject: [PATCH 029/215] [BUG] Fix alpha bug on 3D PathCollection plots. (#25478) * Fix alpha bug on 3D PathCollection plots. * Fix flake8 linting errors * Handle RGB instead of RGBA case. * Clean main if clause. * linting * Add tests for scatter alpha conversion * Simplify logic for depth shading alpha correction * linting * linting * linting * Comments * restore missing line * linting * Fix logic * Apply suggestion from @timhoffm * Fix alpha not having z_marker_idx sorting when applicable --------- Co-authored-by: Scott Shambaugh <14363975+scottshambaugh@users.noreply.github.com> Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Co-authored-by: Scott Shambaugh --- lib/mpl_toolkits/mplot3d/art3d.py | 27 ++++++++++++++----- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 13 +++++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index 6898a8aaf4cf..f664127dcb59 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -819,18 +819,26 @@ def do_3d_projection(self): return np.nan def _maybe_depth_shade_and_sort_colors(self, color_array): - color_array = ( - _zalpha( + # Adjust the color_array alpha values if point depths are defined + # and depth shading is active + alpha = self._alpha + if self._vzs is not None and self._depthshade: + color_array = _zalpha( color_array, self._vzs, min_alpha=self._depthshade_minalpha, ) - if self._vzs is not None and self._depthshade - else color_array - ) + if alpha is not None and color_array.shape[1] == 4: # RGBA, not RGB + alpha = alpha * color_array[:, 3] + + # Adjust the order of the color_array using the _z_markers_idx, + # which has been sorted by z-depth if len(color_array) > 1: color_array = color_array[self._z_markers_idx] - return mcolors.to_rgba_array(color_array, self._alpha) + if np.ndim(alpha) > 0: + alpha = np.asarray(alpha)[self._z_markers_idx] + + return mcolors.to_rgba_array(color_array, alpha) def get_facecolor(self): return self._maybe_depth_shade_and_sort_colors(super().get_facecolor()) @@ -1070,6 +1078,7 @@ def _use_zordered_offset(self): def _maybe_depth_shade_and_sort_colors(self, color_array): # Adjust the color_array alpha values if point depths are defined # and depth shading is active + alpha = self._alpha if self._vzs is not None and self._depthshade: color_array = _zalpha( color_array, @@ -1077,13 +1086,17 @@ def _maybe_depth_shade_and_sort_colors(self, color_array): min_alpha=self._depthshade_minalpha, _data_scale=self._data_scale, ) + if alpha is not None and color_array.shape[1] == 4: # RGBA, not RGB + alpha = alpha * color_array[:, 3] # Adjust the order of the color_array using the _z_markers_idx, # which has been sorted by z-depth if len(color_array) > 1: color_array = color_array[self._z_markers_idx] + if np.ndim(alpha) > 0: + alpha = np.asarray(alpha)[self._z_markers_idx] - return mcolors.to_rgba_array(color_array) + return mcolors.to_rgba_array(color_array, alpha) def get_facecolor(self): return self._maybe_depth_shade_and_sort_colors(super().get_facecolor()) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 078da596a9a4..2a5593a641c9 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -440,6 +440,19 @@ def test_scatter3d_linewidth(): marker='o', linewidth=np.arange(10)) +@check_figures_equal() +def test_scatter3d_cmap_alpha(fig_ref, fig_test): + # Check that alpha is applied correctly with colormapped scatter. + # Regression test for https://github.com/matplotlib/matplotlib/issues/25468 + x, y, z = np.arange(5), np.zeros(5), np.arange(5) + c = np.array([0, 1, np.nan, 3, 4]) + + ax_test = fig_test.add_subplot(projection='3d') + ax_test.scatter(x, y, z, c=c) + ax_ref = fig_ref.add_subplot(projection='3d') + ax_ref.scatter(x, y, z, c=c, alpha=1) + + @check_figures_equal() def test_scatter3d_linewidth_modification(fig_ref, fig_test): # Changing Path3DCollection linewidths with array-like post-creation From ccabde03a6762de3f19ae643f992c156e52fb3b0 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Thu, 7 May 2026 08:04:29 +0100 Subject: [PATCH 030/215] FIX: use axis lines tight bbox within axis artist tight bbox --- lib/mpl_toolkits/axisartist/axis_artist.py | 2 +- .../axisartist/tests/test_axis_artist.py | 29 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/lib/mpl_toolkits/axisartist/axis_artist.py b/lib/mpl_toolkits/axisartist/axis_artist.py index 3ba70c3d7d3b..75dd978eb6f9 100644 --- a/lib/mpl_toolkits/axisartist/axis_artist.py +++ b/lib/mpl_toolkits/axisartist/axis_artist.py @@ -1086,7 +1086,7 @@ def get_tightbbox(self, renderer=None): *self.minor_ticklabels.get_window_extents(renderer), self.label.get_window_extent(renderer), self.offsetText.get_window_extent(renderer), - self.line.get_window_extent(renderer), + self.line.get_tightbbox(renderer), ] bb = [b for b in bb if b and (b.width != 0 or b.height != 0)] if bb: diff --git a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py index 8c67b18c0349..9145ce666f32 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py +++ b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py @@ -1,7 +1,13 @@ +import numpy as np + +import matplotlib as mpl import matplotlib.pyplot as plt +from matplotlib.projections import PolarAxes from matplotlib.testing.decorators import image_comparison +from matplotlib.transforms import Affine2D -from mpl_toolkits.axisartist import AxisArtistHelperRectlinear +from mpl_toolkits.axisartist import (AxisArtistHelperRectlinear, GridHelperCurveLinear, + HostAxes) from mpl_toolkits.axisartist.axis_artist import (AxisArtist, AxisLabel, LabelBase, Ticks, TickLabels) @@ -90,3 +96,24 @@ def test_axis_artist(): axisline.label.set_pad(5) ax.set_ylabel("Test") + + +@mpl.style.context('default') +def test_axisartist_tightbbox(): + fig = plt.figure() + tr = Affine2D().scale(np.pi / 180., 1.) + PolarAxes.PolarTransform() + grid_helper = GridHelperCurveLinear(tr) + ax = fig.add_subplot(axes_class=HostAxes, grid_helper=grid_helper) + ax.axis["lon"] = ax.new_floating_axis(1, 9) + + ax.set_xlim(-5, 12) + ax.set_ylim(-5, 10) + + ax.axis['lon'].major_ticklabels.set_visible(False) + + # Since the labels are invisible and the lines are clipped to the axes, + # the axis's tight bbox should be contained in the axes box. + renderer = fig._get_renderer() + tight_points = ax.axis['lon'].get_tightbbox(renderer).get_points() + for point in tight_points: + assert ax.bbox.contains(*point) From 983dc8eeac9d925bbd005b3c6c9755fc0dd0a2bb Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 7 May 2026 15:56:37 -0400 Subject: [PATCH 031/215] Restore PolarTransform(apply_theta_transforms) parameter This partially reverts commit 6083ecd880c950a705ba74107ff18b447e2c53dc, but with a full deprecation of the parameter as a whole. Fixes #31624 --- doc/api/next_api_changes/deprecations/31630-ES.rst | 10 ++++++++++ doc/api/next_api_changes/removals/30004-DS.rst | 10 ---------- lib/matplotlib/projections/polar.py | 8 ++++++-- lib/matplotlib/projections/polar.pyi | 3 +++ 4 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/31630-ES.rst delete mode 100644 doc/api/next_api_changes/removals/30004-DS.rst diff --git a/doc/api/next_api_changes/deprecations/31630-ES.rst b/doc/api/next_api_changes/deprecations/31630-ES.rst new file mode 100644 index 000000000000..2509b4323022 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/31630-ES.rst @@ -0,0 +1,10 @@ +``apply_theta_transforms`` option in ``PolarTransform`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Applying theta transforms in `~matplotlib.projections.polar.PolarTransform` and +`~matplotlib.projections.polar.InvertedPolarTransform` has been removed, and the +*apply_theta_transforms* keyword argument is deprecated for both classes. + +If you need to retain the behaviour where theta values are transformed, chain the +``PolarTransform`` with a `~matplotlib.transforms.Affine2D` transform that performs the +theta shift and/or sign shift. diff --git a/doc/api/next_api_changes/removals/30004-DS.rst b/doc/api/next_api_changes/removals/30004-DS.rst deleted file mode 100644 index f5fdf214366c..000000000000 --- a/doc/api/next_api_changes/removals/30004-DS.rst +++ /dev/null @@ -1,10 +0,0 @@ -``apply_theta_transforms`` option in ``PolarTransform`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Applying theta transforms in `~matplotlib.projections.polar.PolarTransform` and -`~matplotlib.projections.polar.InvertedPolarTransform` has been removed, and -the ``apply_theta_transforms`` keyword argument removed from both classes. - -If you need to retain the behaviour where theta values -are transformed, chain the ``PolarTransform`` with a `~matplotlib.transforms.Affine2D` -transform that performs the theta shift and/or sign shift. diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 75e1295f77f1..9d999dde2f6f 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -34,7 +34,9 @@ class PolarTransform(mtransforms.Transform): input_dims = output_dims = 2 - def __init__(self, axis=None, use_rmin=True, *, scale_transform=None): + @_api.delete_parameter('3.11', 'apply_theta_transforms') + def __init__(self, axis=None, use_rmin=True, *, + apply_theta_transforms=False, scale_transform=None): """ Parameters ---------- @@ -183,7 +185,9 @@ class InvertedPolarTransform(mtransforms.Transform): """ input_dims = output_dims = 2 - def __init__(self, axis=None, use_rmin=True): + @_api.delete_parameter('3.11', 'apply_theta_transforms') + def __init__(self, axis=None, use_rmin=True, + *, apply_theta_transforms=False): """ Parameters ---------- diff --git a/lib/matplotlib/projections/polar.pyi b/lib/matplotlib/projections/polar.pyi index fc1d508579b5..de1cbc293900 100644 --- a/lib/matplotlib/projections/polar.pyi +++ b/lib/matplotlib/projections/polar.pyi @@ -18,6 +18,7 @@ class PolarTransform(mtransforms.Transform): axis: PolarAxes | None = ..., use_rmin: bool = ..., *, + apply_theta_transforms: bool = ..., scale_transform: mtransforms.Transform | None = ..., ) -> None: ... def inverted(self) -> InvertedPolarTransform: ... @@ -34,6 +35,8 @@ class InvertedPolarTransform(mtransforms.Transform): self, axis: PolarAxes | None = ..., use_rmin: bool = ..., + *, + apply_theta_transforms: bool = ..., ) -> None: ... def inverted(self) -> PolarTransform: ... From b2df08a78127287179f519eb0d05ddc5fcc3770c Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 7 May 2026 22:00:05 +0200 Subject: [PATCH 032/215] DOC: heading style (#31591) --- doc/devel/coding_guide.rst | 4 ++-- doc/devel/min_dep_policy.rst | 4 ++-- doc/devel/release_guide.rst | 10 ++++---- doc/devel/style_guide.rst | 45 ++++++++++++++++++++++++++++++++++++ doc/devel/testing.rst | 34 +++++++++++++-------------- doc/devel/triage.rst | 10 ++++---- 6 files changed, 75 insertions(+), 32 deletions(-) diff --git a/doc/devel/coding_guide.rst b/doc/devel/coding_guide.rst index fe7769909368..7a4b296b52ce 100644 --- a/doc/devel/coding_guide.rst +++ b/doc/devel/coding_guide.rst @@ -190,8 +190,8 @@ local arguments and the rest are passed on as .. _using_logging: -Using logging for debug messages -================================ +Use logging for debug messages +============================== Matplotlib uses the standard Python `logging` library to write verbose warnings, information, and debug messages. Please use it! In all those places diff --git a/doc/devel/min_dep_policy.rst b/doc/devel/min_dep_policy.rst index 81a84491bc4a..517cc872139e 100644 --- a/doc/devel/min_dep_policy.rst +++ b/doc/devel/min_dep_policy.rst @@ -157,8 +157,8 @@ Matplotlib Python NumPy .. _`1.3`: https://matplotlib.org/1.3.0/users/installing.html#build-requirements -Updating Python and NumPy versions -================================== +Update Python and NumPy versions +================================ To update the minimum versions of Python we need to update: diff --git a/doc/devel/release_guide.rst b/doc/devel/release_guide.rst index ccac5b4f8872..eefc31aec07c 100644 --- a/doc/devel/release_guide.rst +++ b/doc/devel/release_guide.rst @@ -45,7 +45,7 @@ versioning scheme: *macro.meso.micro*. .. _release_feature_freeze: -Making the release branch +Create the release branch ========================= .. note:: @@ -379,8 +379,8 @@ to the VER-doc branch and push to GitHub. :: .. _release_bld_bin: -Building binaries -================= +Build binaries +============== We distribute macOS, Windows, and many Linux wheels as well as a source tarball via PyPI. @@ -412,8 +412,8 @@ PyPI. .. _release_upload_bin: -Manually uploading to PyPI -========================== +Manual upload to PyPI +===================== .. note:: diff --git a/doc/devel/style_guide.rst b/doc/devel/style_guide.rst index e35112a65e42..b260872557c5 100644 --- a/doc/devel/style_guide.rst +++ b/doc/devel/style_guide.rst @@ -176,6 +176,51 @@ reliability and consistency in documentation. They are not interchangeable. .. |Axis| replace:: :class:`~matplotlib.axis.Axis` +Headings +-------- +Use sentence case for headings. + +.. table:: + :width: 100% + :widths: 50, 50 + + +------------------------------------+------------------------------------+ + | Correct | Incorrect | + +====================================+====================================+ + | Quick start guide | Quick Start Guide | + +------------------------------------+------------------------------------+ + +Noun phrases and verb phrases are both acceptable for headings. Noun phrases +are preferred for higher-level headings and descriptive sections as they +simply state the content. + +.. table:: + :width: 100% + :widths: 50, 50 + + +------------------------------------+------------------------------------+ + | Correct | Incorrect | + +====================================+====================================+ + | Bug triage and issue curation | Triage bugs and curate issues | + +------------------------------------+------------------------------------+ + +Verb phrases are preferred for instructive and action-oriented sections; in +particular when they cover steps in a process, such as the subsections in +:ref:`installing_for_devs`. + +Use the second-person imperative form of the verb rather than the gerund form. + +.. table:: + :width: 100% + :widths: 50, 50 + + +------------------------------------+------------------------------------+ + | Correct | Incorrect | + +====================================+====================================+ + | Fork the Matplotlib repository | Forking the Matplotlib repository | + +------------------------------------+------------------------------------+ + + Grammar ------- diff --git a/doc/devel/testing.rst b/doc/devel/testing.rst index 0b9390477b10..990b9d0b6493 100644 --- a/doc/devel/testing.rst +++ b/doc/devel/testing.rst @@ -13,10 +13,8 @@ testing infrastructure are in :mod:`matplotlib.testing`. .. _pytest-xdist: https://pypi.org/project/pytest-xdist/ -.. _testing_requirements: - -Requirements ------------- +Prerequisites +------------- To run the tests you will need to :ref:`set up Matplotlib for development `. Note in @@ -34,8 +32,8 @@ particular the :ref:`additional dependencies ` for testing. .. _run_tests: -Running the tests ------------------ +Run the tests +------------- In the root directory of your development repository run:: @@ -82,8 +80,8 @@ to avoid clashes between ``pytest``'s import mode and Python's search path: python -m pytest --import-mode prepend -Viewing image test output -^^^^^^^^^^^^^^^^^^^^^^^^^ +View image test output +^^^^^^^^^^^^^^^^^^^^^^ The output of :ref:`image-based ` tests is stored in a ``result_images`` directory. These images can be compiled into one HTML page, containing @@ -100,8 +98,8 @@ to the folder where the baseline test images are stored. The triage tool require :ref:`QT ` is installed. -Writing tests -------------- +Write tests +----------- Tests are located in :file:`lib/matplotlib/tests`. They are organized to mirror the structure of the code in :file:`lib/matplotlib`. For example, tests for the ``mathtext.py`` module are in :file:`lib/matplotlib/tests/test_mathtext.py`. @@ -284,8 +282,8 @@ See the documentation of `~matplotlib.testing.decorators.image_comparison` and `~matplotlib.testing.decorators.check_figures_equal` for additional information about their use. -Using GitHub Actions for CI ---------------------------- +CI with GitHub Actions +---------------------- `GitHub Actions `_ is a hosted CI system "in the cloud". @@ -311,8 +309,8 @@ https://github.com/your_GitHub_user_name/matplotlib/actions -- here's `an example `_. -Using tox ---------- +tox: Test multiple python versions +---------------------------------- `Tox `_ is a tool for running tests against multiple Python environments, including multiple versions of Python @@ -352,8 +350,8 @@ tests are run. For more info on the ``tox.ini`` file, see the `Tox Configuration Specification `_. -Building old versions of Matplotlib ------------------------------------ +Build old versions of Matplotlib +-------------------------------- When running a ``git bisect`` to see which commit introduced a certain bug, you may (rarely) need to build very old versions of Matplotlib. The following @@ -361,8 +359,8 @@ constraints need to be taken into account: - Matplotlib 1.3 (or earlier) requires numpy 1.8 (or earlier). -Testing released versions of Matplotlib ---------------------------------------- +Test released versions of Matplotlib +------------------------------------ Running the tests on an installation of a released version (e.g. PyPI package or conda package) also requires additional setup. diff --git a/doc/devel/triage.rst b/doc/devel/triage.rst index ca06fd515c79..de27afcab111 100644 --- a/doc/devel/triage.rst +++ b/doc/devel/triage.rst @@ -1,9 +1,9 @@ .. _bug_triaging: -******************************* -Bug triaging and issue curation -******************************* +***************************** +Bug triage and issue curation +***************************** The `issue tracker `_ is important to communication in the project because it serves as the @@ -107,8 +107,8 @@ important tasks: question or has been considered as unclear for many years, then it should be closed. -Preparing PRs for review -======================== +Prepare PRs for review +====================== Reviewing code is also encouraged. Contributors and users are welcome to participate to the review process following our :ref:`review guidelines From e354866d03ada904ebec9acefad23a1c2067fa54 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 8 May 2026 00:52:25 +0200 Subject: [PATCH 033/215] FIX: Prohibit special TeX chars in pgf metadata They would have raised or been swallowed anyway. Note: One could try to escape some chars, but it's quite unclear how hyperref handles the parameters, It seems that escaping does not systematically work. --- lib/matplotlib/backends/backend_pgf.py | 7 +++++++ lib/matplotlib/tests/test_backend_pgf.py | 22 +++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 3205f294ab2d..d8b29f750dfa 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -154,6 +154,13 @@ def _metadata_to_str(key, value): value = value.name.decode('ascii') else: value = str(value) + + invalid_chars = r"\{}[]()" + if any(c in value for c in invalid_chars): + raise ValueError( + f"Invalid metadata value for {key!r}: {value!r}. " + f"The value must not contain the chars {invalid_chars}.") + return f'{key}={{{value}}}' diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index e5b73c9450f3..4af329fa28d4 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -15,7 +15,7 @@ from matplotlib.testing import _has_tex_package, _check_for_pgf from matplotlib.testing.exceptions import ImageComparisonFailure from matplotlib.testing.compare import compare_images -from matplotlib.backends.backend_pgf import PdfPages +from matplotlib.backends.backend_pgf import _metadata_to_str, PdfPages from matplotlib.testing.decorators import ( _image_directories, check_figures_equal, image_comparison) from matplotlib.testing._markers import ( @@ -37,6 +37,26 @@ def compare_figure(fname, savefig_kwargs={}, tol=0): raise ImageComparisonFailure(err) +@pytest.mark.parametrize("key, value, expected_str", [ + ("Author", "me", "Author={me}"), + ("ModDate", + datetime.datetime(1968, 8, 1, tzinfo=datetime.timezone(datetime.timedelta(0))), + "ModDate={D:19680801000000Z}"), +]) +def test__metadata_to_str(key, value, expected_str): + assert _metadata_to_str(key, value) == expected_str + + +@pytest.mark.parametrize("value", [ + r"Backslashes, e.g. in \commands", + r"funny braces {}", + r"and square brackets]", +]) +def test__metadata_to_str_error(value): + with pytest.raises(ValueError, match="value must not contain the chars"): + _metadata_to_str("Title", value) + + @needs_pgf_xelatex @needs_ghostscript @pytest.mark.backend('pgf') From c2730be1dcc8d4e21013ed37751863b260668e61 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 8 May 2026 06:17:30 +0200 Subject: [PATCH 034/215] Update lib/matplotlib/backends/backend_pgf.py Co-authored-by: Thomas A Caswell --- lib/matplotlib/backends/backend_pgf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index d8b29f750dfa..c00a1041eacc 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -156,7 +156,7 @@ def _metadata_to_str(key, value): value = str(value) invalid_chars = r"\{}[]()" - if any(c in value for c in invalid_chars): + if any(c in value + key for c in invalid_chars): raise ValueError( f"Invalid metadata value for {key!r}: {value!r}. " f"The value must not contain the chars {invalid_chars}.") From 79bee9bf418c35ed0d6bd9e760c2facf91905ed1 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 8 May 2026 02:12:10 -0400 Subject: [PATCH 035/215] TYP: Add missing default value to _api.getitem_checked --- lib/matplotlib/_api/__init__.pyi | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/matplotlib/_api/__init__.pyi b/lib/matplotlib/_api/__init__.pyi index cf24edf65db5..58dee136e233 100644 --- a/lib/matplotlib/_api/__init__.pyi +++ b/lib/matplotlib/_api/__init__.pyi @@ -42,9 +42,7 @@ def check_isinstance( def list_suggestion_error_msg(name: str, potential: Any, values: Sequence[Any]) -> str: ... def check_in_list(values: Sequence[Any], /, **kwargs: Any) -> None: ... def check_shape(shape: tuple[int | None, ...], /, **kwargs: NDArray) -> None: ... -def getitem_checked( - mapping: Mapping[Any, _T], /, _error_cls: type[Exception], **kwargs: Any -) -> _T: ... +def getitem_checked(mapping: Mapping[Any, _T], /, _error_cls: type[Exception] = ..., **kwargs: Any) -> _T: ... def caching_module_getattr(cls: type) -> Callable[[str], Any]: ... @overload def define_aliases( From e4906261750e55751491f8ace5017d62231a71d6 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 8 May 2026 04:29:00 -0400 Subject: [PATCH 036/215] Fix some font-related typing issues - Some places take/emit a `FontPath` and not a `str` any more. - The module-level `findfont` is a copy of an instance method, so it should match that as well. - Also add some private things, because they help find some additional bugs, and at least `_find_fonts_by_props` is possibly going to be public later. --- lib/matplotlib/_mathtext.py | 2 +- lib/matplotlib/backend_bases.pyi | 3 ++- lib/matplotlib/backends/_backend_pdf_ps.py | 5 +++-- lib/matplotlib/font_manager.pyi | 14 +++++++++++--- lib/matplotlib/ft2font.pyi | 3 ++- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index b04386e7666a..17dc1b8fb462 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -1721,8 +1721,8 @@ def ship(box: Box, xy: tuple[float, float] = (0, 0)) -> Output: off_h = ox off_v = oy + box.height output = Output(box) - phantom: list[bool] = [] + def render(node, *args): if not any(phantom): node.render(*args) diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index a69b36093839..94a8522717cd 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -15,7 +15,7 @@ from matplotlib.figure import Figure from matplotlib.font_manager import FontProperties from matplotlib.path import Path from matplotlib.texmanager import TexManager -from matplotlib.text import Text +from matplotlib.text import Text, TextToPath from matplotlib.transforms import Bbox, BboxBase, Transform, TransformedPath from collections.abc import Callable, Iterable, Sequence @@ -40,6 +40,7 @@ def register_backend( def get_registered_canvas_class(format: str) -> type[FigureCanvasBase]: ... class RendererBase: + _text2path: TextToPath def __init__(self) -> None: ... def open_group(self, s: str, gid: str | None = ...) -> None: ... def close_group(self, s: str) -> None: ... diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index a06779b8efee..87fbae4d749b 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -18,6 +18,7 @@ if typing.TYPE_CHECKING: + from .font_manager import FontPath from .ft2font import CharacterCodeType, FT2Font, GlyphIndexType from fontTools.ttLib import TTFont @@ -34,7 +35,7 @@ def _cached_get_afm_from_fname(fname): return AFM(fh) -def get_glyphs_subset(fontfile: str, glyphs: set[GlyphIndexType]) -> TTFont: +def get_glyphs_subset(fontfile: FontPath, glyphs: set[GlyphIndexType]) -> TTFont: """ Subset a TTF font. @@ -199,7 +200,7 @@ def __init__(self, subset_size: int = 0): self.subset_size = subset_size def track(self, font: FT2Font, s: str, - features: tuple[str, ...] | None = ..., + features: tuple[str, ...] | None = None, language: str | tuple[tuple[str, int, int], ...] | None = None ) -> list[tuple[int, CharacterCodeType]]: """ diff --git a/lib/matplotlib/font_manager.pyi b/lib/matplotlib/font_manager.pyi index b5c131d33702..4bb1a8bae2a9 100644 --- a/lib/matplotlib/font_manager.pyi +++ b/lib/matplotlib/font_manager.pyi @@ -133,11 +133,19 @@ class FontManager: self, prop: str | FontProperties, fontext: Literal["ttf", "afm"] = ..., - directory: str | None = ..., + directory: str | os.PathLike | None = ..., fallback_to_default: bool = ..., rebuild_if_missing: bool = ..., ) -> FontPath: ... def get_font_names(self) -> list[str]: ... + def _find_fonts_by_props( + self, + prop: str | FontProperties, + fontext: Literal["ttf", "afm"] = ..., + directory: str | os.PathLike | None = ..., + fallback_to_default: bool = ..., + rebuild_if_missing: bool = ..., + ) -> list[FontPath]: ... def is_opentype_cff_font(filename: str) -> bool: ... def get_font( @@ -149,8 +157,8 @@ fontManager: FontManager def findfont( prop: str | FontProperties, fontext: Literal["ttf", "afm"] = ..., - directory: str | None = ..., + directory: str | os.PathLike | None = ..., fallback_to_default: bool = ..., rebuild_if_missing: bool = ..., -) -> str: ... +) -> FontPath: ... def get_font_names() -> list[str]: ... diff --git a/lib/matplotlib/ft2font.pyi b/lib/matplotlib/ft2font.pyi index 05f987292ffc..f8057742b376 100644 --- a/lib/matplotlib/ft2font.pyi +++ b/lib/matplotlib/ft2font.pyi @@ -239,7 +239,8 @@ class FT2Font(Buffer): *, face_index: int = ..., _fallback_list: list[FT2Font] | None = ..., - _kerning_factor: int | None = ... + _kerning_factor: int | None = ..., + _warn_if_used: bool = ..., ) -> None: ... if sys.version_info[:2] >= (3, 12): def __buffer__(self, /, flags: int) -> memoryview: ... From faf35f31e748991dd64956486e58ff495af639c8 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 8 May 2026 04:54:16 -0400 Subject: [PATCH 037/215] Fix RendererCairo._draw_mathtext implementation It wasn't updated to handle the change to `VectorParse` in #30335. --- lib/matplotlib/backends/backend_cairo.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index a62890d7c3b1..a16c7a25aec2 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -254,7 +254,10 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): ctx.new_path() ctx.select_font_face(*_cairo_font_args_from_font_prop(ttfFontProperty(font))) ctx.set_font_size(self.points_to_pixels(fontsize)) - ctx.show_glyphs([(idx, ox, -oy) for _, _, idx, ox, oy in font_glyphs]) + ctx.show_glyphs([ + (glyph_index, ox, -oy) + for _font, _size, _ccode, glyph_index, ox, oy in font_glyphs + ]) for ox, oy, w, h in rects: ctx.new_path() From 0260760d18d5584e46d1bde52d9faa8e16026128 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 8 May 2026 04:56:24 -0400 Subject: [PATCH 038/215] Fix undefined method in `dviread.Text.glyph_name_or_index` The method was removed in #29939, but this call site was forgotten. --- lib/matplotlib/dviread.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index e41618d67579..979744d1ef5c 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -121,7 +121,7 @@ def glyph_name_or_index(self): # control all involved versions and are deeply familiar with the # implementation", but a further mapping bug was fixed in luaotfload # commit 8f2dca4, first included in v3.23). - entry = self._get_pdftexmap_entry() + entry = PsfontsMap(find_tex_file("pdftex.map"))[self.font.texname] return (_parse_enc(entry.encoding)[self.glyph] if entry.encoding is not None else self.glyph) From 53d20a9cd26e8f92ce63e36ac4d3b690146925fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 8 May 2026 19:13:16 +0000 Subject: [PATCH 039/215] Bump the actions group with 2 updates Bumps the actions group with 2 updates: [github/codeql-action](https://github.com/github/codeql-action) and [actions/labeler](https://github.com/actions/labeler). Updates `github/codeql-action` from 4.35.3 to 4.35.4 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/e46ed2cbd01164d986452f91f178727624ae40d7...68bde559dea0fdcac2102bfdf6230c5f70eb485e) Updates `actions/labeler` from 6.0.1 to 6.1.0 - [Release notes](https://github.com/actions/labeler/releases) - [Commits](https://github.com/actions/labeler/compare/634933edcd8ababfe52f92936142cc22ac488b1b...f27b608878404679385c85cfa523b85ccb86e213) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 4.35.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: actions/labeler dependency-version: 6.1.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/labeler.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 9ee6ac545967..71425e9cc3e9 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -34,7 +34,7 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 + uses: github/codeql-action/init@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 with: languages: ${{ matrix.language }} @@ -45,4 +45,4 @@ jobs: pip install --user -v . - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3 + uses: github/codeql-action/analyze@68bde559dea0fdcac2102bfdf6230c5f70eb485e # v4.35.4 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 2914c64a8461..600e7fc34a95 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -12,6 +12,6 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 + - uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0 with: sync-labels: true From 13f2d218e7f14e92a22209bde2f8a82653fd8436 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sat, 9 May 2026 03:48:42 +0200 Subject: [PATCH 040/215] Apply suggestion from @timhoffm --- lib/matplotlib/backends/backend_pgf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index c00a1041eacc..36048fe016df 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -155,6 +155,8 @@ def _metadata_to_str(key, value): else: value = str(value) + # ensure that metadata does not contain special TeX chars because we + # insert the metadata as raw text into the TeX source invalid_chars = r"\{}[]()" if any(c in value + key for c in invalid_chars): raise ValueError( From 0302a278459d9c6b0429c5836b0137c5505db78b Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sat, 9 May 2026 12:41:20 +0200 Subject: [PATCH 041/215] DOC: Improve autoscaling and margin docs (#31609) * DOC: Improve autoscaling and margin docs * Apply suggestions from code review Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --------- Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --- galleries/users_explain/axes/autoscale.py | 44 +++++++++++++---------- lib/matplotlib/axes/_base.py | 40 ++++++++++++++++++--- lib/matplotlib/axis.py | 9 +++++ 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/galleries/users_explain/axes/autoscale.py b/galleries/users_explain/axes/autoscale.py index 337960302c38..ea0c2d24c55a 100644 --- a/galleries/users_explain/axes/autoscale.py +++ b/galleries/users_explain/axes/autoscale.py @@ -6,33 +6,41 @@ Axis autoscaling ================ -The limits on an axis can be set manually (e.g. ``ax.set_xlim(xmin, xmax)``) -or Matplotlib can set them automatically based on the data already on the Axes. -There are a number of options to this autoscaling behaviour, discussed below. -""" +Basic concept +------------- -# %% -# We will start with a simple line plot showing that autoscaling -# extends the axis limits 5% beyond the data limits (-2π, 2π). +Autoscaling ensures that data is visible within the Axes by automatically adjusting +the axis limits. When you plot data, Matplotlib's autoscaling mechanism updates the +axis limits accordingly. +""" import matplotlib.pyplot as plt import numpy as np -x = np.linspace(-2 * np.pi, 2 * np.pi, 100) +x = np.linspace(-6, 6, 201) y = np.sinc(x) fig, ax = plt.subplots() ax.plot(x, y) # %% +# +# .. _autoscale_margins: +# # Margins # ------- -# The default margin around the data limits is 5%, which is based on the -# default configuration setting of :rc:`axes.xmargin`, :rc:`axes.ymargin`, -# and :rc:`axes.zmargin`: +# To ensure that the data is not at the very edge of the plot, Matplotlib adds a +# margin around the data limits. Note that the *x* data range in the above plot is +# [-6, 6], but the x-axis limits are slightly wider due to the margin. +# +# The default margin is 5%, defined via +# +# - :rc:`axes.xmargin` +# - :rc:`axes.ymargin` +# - :rc:`axes.zmargin` -print(ax.margins()) +print(ax.get_xmargin(), ax.get_ymargin()) # %% # The margin size can be overridden to make them smaller or larger using @@ -116,14 +124,14 @@ ax[1].set_title("Two curves") # %% -# However, there are cases when you don't want to automatically adjust the -# viewport to new data. +# If you don't want automatic updates of the axis limits, either deactivate +# autoscaling with `~.axes.Axes.autoscale` or set the limits +# manually with `~.axes.Axes.set_xlim` / `~.axes.Axes.set_ylim`. # -# One way to disable autoscaling is to manually set the -# axis limit. Let's say that we want to see only a part of the data in +# Let's say that we want to see only a part of the data in # greater detail. Setting the ``xlim`` persists even if we add more curves to -# the data. To recalculate the new limits calling `.Axes.autoscale` will -# toggle the functionality manually. +# the data. Calling `.Axes.autoscale` will re-enable the autoscaling and +# recalculate the limits to fit all the data. fig, ax = plt.subplots(ncols=2, figsize=(12, 8)) ax[0].plot(x, y) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index ee933ea138ad..0ddf18b12ec2 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2787,6 +2787,13 @@ def set_autoscale_on(self, b): Parameters ---------- b : bool + + See Also + -------- + :ref:`autoscale` + matplotlib.axes.Axes.autoscale + matplotlib.axes.Axes.set_autoscalex_on + matplotlib.axes.Axes.set_autoscaley_on """ for axis in self._axis_map.values(): axis._set_autoscale_on(b) @@ -2825,6 +2832,7 @@ def get_xmargin(self): See Also -------- + :ref:`autoscale_margins` matplotlib.axes.Axes.set_xmargin """ return self._xmargin @@ -2841,6 +2849,7 @@ def get_ymargin(self): See Also -------- + :ref:`autoscale_margins` matplotlib.axes.Axes.set_ymargin """ return self._ymargin @@ -2860,6 +2869,13 @@ def set_xmargin(self, m): Parameters ---------- m : float greater than -0.5 + + See Also + -------- + :ref:`autoscale_margins` + matplotlib.axes.Axes.margins + matplotlib.axes.Axes.get_xmargin + """ if m <= -0.5: raise ValueError("margin must be greater than -0.5") @@ -2882,6 +2898,12 @@ def set_ymargin(self, m): Parameters ---------- m : float greater than -0.5 + + See Also + -------- + :ref:`autoscale_margins` + matplotlib.axes.Axes.margins + matplotlib.axes.Axes.get_ymargin """ if m <= -0.5: raise ValueError("margin must be greater than -0.5") @@ -2945,6 +2967,7 @@ def margins(self, *margins, x=None, y=None, tight=True): See Also -------- + :ref:`autoscale_margins` .Axes.set_xmargin, .Axes.set_ymargin """ @@ -2999,10 +3022,12 @@ def autoscale(self, enable=True, axis='both', tight=None): """ Autoscale the axis view to the data (toggle). - Convenience method for simple axis view autoscaling. - It turns autoscaling on or off, and then, - if autoscaling for either axis is on, it performs - the autoscaling on the specified axis or Axes. + Convenience method for simple axis view autoscaling. This: + + - Turns autoscaling on or off (`~.axes.Axes.set_autoscalex_on` / + `~.axes.Axes.set_autoscaley_on`). + - Ensures that view limits will get updated when needed. - As view limits + are lazy-updated, this technically marks the view limits as stale. Parameters ---------- @@ -3016,6 +3041,13 @@ def autoscale(self, enable=True, axis='both', tight=None): If True, first set the margins to zero. Then, this argument is forwarded to `~.axes.Axes.autoscale_view` (regardless of its value); see the description of its behavior there. + + See Also + -------- + :ref:`autoscale` + matplotlib.axes.Axes.set_autoscale_on + matplotlib.axes.Axes.set_autoscalex_on + matplotlib.axes.Axes.set_autoscaley_on """ if enable is None: scalex = True diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 48bbac9264ae..c526b8a2aa6a 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -838,6 +838,15 @@ def _set_autoscale_on(self, b): Parameters ---------- b : bool + + See Also + -------- + matplotlib.axes.Axes.autoscale + matplotlib.axes.Axes.set_autoscale_on + matplotlib.axes.Axes.get_autoscalex_on + matplotlib.axes.Axes.set_autoscalex_on + matplotlib.axes.Axes.get_autoscaley_on + matplotlib.axes.Axes.set_autoscaley_on """ if b is not None: self._autoscale_on = b From 56d23737ce4983aa80b0b7176fa58b59bd90c984 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sat, 9 May 2026 13:11:54 +0200 Subject: [PATCH 042/215] DOC: Document that bar() errorbars do not support individual coloring (#31579) * DOC: Document that bar() errorbars do not support individual coloring Closes #14480. * Apply suggestions from code review Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> * Apply suggestion from @timhoffm --- lib/matplotlib/axes/_axes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index d3b93ed2c40a..75dcb4653c52 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2410,11 +2410,16 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", errors. - *None*: No errorbar. (Default) - See :doc:`/gallery/statistics/errorbar_features` for an example on + This is a convenience shortcut for an extra `~.axes.Axes.errorbar` + call. See its documentation and + :doc:`/gallery/statistics/errorbar_features` for an example on the usage of *xerr* and *yerr*. ecolor : :mpltype:`color` or list of :mpltype:`color`, default: 'black' The line color of the errorbars. + Multiple colors are only supported if the errorbars do not have + caps. If you need individually colored errorbars with caps, instead + use explicit `~.axes.Axes.errorbar` calls for each data point. capsize : float, default: :rc:`errorbar.capsize` The length of the error bar caps in points. From c4d52bf13c922f36e556ac83d65fcb3c909d5536 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 10 May 2026 10:53:57 +0200 Subject: [PATCH 043/215] FIX: Pin rstcheck to prevent CI failure While rstcheck itself is frozen and also was not updated, it depends on rstcheck-core, which was recently updated to 1.3 and contains a bug that results in a false error. https://github.com/rstcheck/rstcheck-core/pull/114#pullrequestreview-4239740896 This PR excludes that version to fix pre-commit rstcheck in our CI. --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7de40cf539ea..4602570328a9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -70,6 +70,7 @@ repos: hooks: - id: rstcheck additional_dependencies: + - rstcheck-core!=1.3 # https://github.com/rstcheck/rstcheck-core/pull/114#pullrequestreview-4239740896 - sphinx>=1.8.1 - tomli - repo: https://github.com/adrienverge/yamllint From 34df90830df39ae772e5eeada2355b1cd3a96d44 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 10 May 2026 11:01:17 +0200 Subject: [PATCH 044/215] MNT: Remove pre-commit/rstcheck dependency on tomli This was only needed to support python<=3.11 and is thus not needed anymore on the main branch. I'm unclear whether we should also remove `sphinx>=1.8.1`. It's far off and I'm quite sure we do not systematically specify lower compatibility boundaries. --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7de40cf539ea..b018c470244b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -71,7 +71,6 @@ repos: - id: rstcheck additional_dependencies: - sphinx>=1.8.1 - - tomli - repo: https://github.com/adrienverge/yamllint rev: cba56bcde1fdd01c1deb3f945e69764c291a6530 # frozen: v1.38.0 hooks: From dd89dfddc1312053400c65196d8f8ab8f58b5253 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 10 May 2026 15:43:13 +0200 Subject: [PATCH 045/215] DOC: Prevent ticks from being cut off in tick rotation example (#31649) * DOC: Prevent ticks from being cut off in tick rotation example * Update galleries/examples/ticks/ticklabels_rotation.py Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --------- Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --- galleries/examples/ticks/ticklabels_rotation.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/galleries/examples/ticks/ticklabels_rotation.py b/galleries/examples/ticks/ticklabels_rotation.py index c17312a61d48..f5ab80745630 100644 --- a/galleries/examples/ticks/ticklabels_rotation.py +++ b/galleries/examples/ticks/ticklabels_rotation.py @@ -7,6 +7,9 @@ Adjust the tick properties using `~.Axes.tick_params`: Set the angle in degrees via the *rotation* parameter. Set the *rotation_mode* parameter to "xtick" / "ytick" to make the text point towards the tick, see also `~.Text.set_rotation_mode`. + +Note: We use ``layout="constrained"`` to make sure there is enough space for the tick +labels so that they are not cut off. """ import matplotlib.pyplot as plt @@ -30,7 +33,7 @@ 'Vietnam': 102.3, } -fig, ax = plt.subplots() +fig, ax = plt.subplots(layout="constrained") ax.bar(population.keys(), population.values()) ax.tick_params("x", rotation=45, rotation_mode="xtick") ax.set_ylabel("population (millions)") From 8ac4071601ef19fae50d4f3825b646595e24d7f7 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sun, 25 May 2025 16:29:38 +0100 Subject: [PATCH 046/215] Fix constrained layout applying pad multiple times --- .../next_api_changes/behavior/30108-REC.rst | 6 +++ lib/matplotlib/_constrained_layout.py | 41 +++++++--------- .../constrained_layout12.png | Bin 12369 -> 31832 bytes .../constrained_layout17.png | Bin 8786 -> 21245 bytes .../constrained_layout8.png | Bin 6995 -> 9221 bytes .../test_submerged_with_colorbar.png | Bin 0 -> 6486 bytes .../tests/test_constrainedlayout.py | 44 ++++++++++++++++++ 7 files changed, 66 insertions(+), 25 deletions(-) create mode 100644 doc/api/next_api_changes/behavior/30108-REC.rst create mode 100644 lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_submerged_with_colorbar.png diff --git a/doc/api/next_api_changes/behavior/30108-REC.rst b/doc/api/next_api_changes/behavior/30108-REC.rst new file mode 100644 index 000000000000..ce4fb0833207 --- /dev/null +++ b/doc/api/next_api_changes/behavior/30108-REC.rst @@ -0,0 +1,6 @@ +Complex layouts and constrained layout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Constrained layout now produces smaller spacing between subplots in some +circumstances. This should only affect complex layouts where rows or columns +contain different numbers of subplots, for example a layout created with +``plt.subplot_mosaic('AC;BC', layout='constrained')``. diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index 33ec8ef985e7..ce488d555898 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -539,14 +539,10 @@ def match_submerged_margins(layoutgrids, fig): # interior columns: if len(ss1.colspan) > 1: - maxsubl = np.max( - lg1.margin_vals['left'][ss1.colspan[1:]] + - lg1.margin_vals['leftcb'][ss1.colspan[1:]] - ) - maxsubr = np.max( - lg1.margin_vals['right'][ss1.colspan[:-1]] + - lg1.margin_vals['rightcb'][ss1.colspan[:-1]] - ) + leftcb = lg1.margin_vals['leftcb'][ss1.colspan[1:]] + rightcb = lg1.margin_vals['rightcb'][ss1.colspan[:-1]] + maxsubl = np.max(lg1.margin_vals['left'][ss1.colspan[1:]] + leftcb) + maxsubr = np.max(lg1.margin_vals['right'][ss1.colspan[:-1]] + rightcb) for ax2 in axs: ss2 = ax2.get_subplotspec() lg2 = layoutgrids[ss2.get_gridspec()] @@ -561,22 +557,17 @@ def match_submerged_margins(layoutgrids, fig): lg2.margin_vals['rightcb'][ss2.colspan[:-1]]) if maxsubr2 > maxsubr: maxsubr = maxsubr2 - for i in ss1.colspan[1:]: - lg1.edit_margin_min('left', maxsubl, cell=i) - for i in ss1.colspan[:-1]: - lg1.edit_margin_min('right', maxsubr, cell=i) + for i, cb in zip(ss1.colspan[1:], leftcb): + lg1.edit_margin_min('left', maxsubl - cb, cell=i) + for i, cb in zip(ss1.colspan[:-1], rightcb): + lg1.edit_margin_min('right', maxsubr - cb, cell=i) # interior rows: if len(ss1.rowspan) > 1: - maxsubt = np.max( - lg1.margin_vals['top'][ss1.rowspan[1:]] + - lg1.margin_vals['topcb'][ss1.rowspan[1:]] - ) - maxsubb = np.max( - lg1.margin_vals['bottom'][ss1.rowspan[:-1]] + - lg1.margin_vals['bottomcb'][ss1.rowspan[:-1]] - ) - + topcb = lg1.margin_vals['topcb'][ss1.rowspan[1:]] + bottomcb = lg1.margin_vals['bottomcb'][ss1.rowspan[:-1]] + maxsubt = np.max(lg1.margin_vals['top'][ss1.rowspan[1:]] + topcb) + maxsubb = np.max(lg1.margin_vals['bottom'][ss1.rowspan[:-1]] + bottomcb) for ax2 in axs: ss2 = ax2.get_subplotspec() lg2 = layoutgrids[ss2.get_gridspec()] @@ -590,10 +581,10 @@ def match_submerged_margins(layoutgrids, fig): lg2.margin_vals['bottom'][ss2.rowspan[:-1]] + lg2.margin_vals['bottomcb'][ss2.rowspan[:-1]] ), maxsubb]) - for i in ss1.rowspan[1:]: - lg1.edit_margin_min('top', maxsubt, cell=i) - for i in ss1.rowspan[:-1]: - lg1.edit_margin_min('bottom', maxsubb, cell=i) + for i, cb in zip(ss1.rowspan[1:], topcb): + lg1.edit_margin_min('top', maxsubt - cb, cell=i) + for i, cb in zip(ss1.rowspan[:-1], bottomcb): + lg1.edit_margin_min('bottom', maxsubb - cb, cell=i) return axs diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout12.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout12.png index 6f5625ae26056dd64836efa4690dfc3e64480faa..b4565e4c5c18f99cd524d90f2cff147ebd89059a 100644 GIT binary patch literal 31832 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 literal 8786 zcma)hcT^O?v+gVlOIk!EEm@)CjNIQUL%!qkB!$6aYv- z06-8Zkg$Y=R%8VJx#g{8;ce!2-`mgD(*ZEF^>%l0^LBB%chT3u)62=tRZ2`kLJTWr z@8BydeMQ*uik-Bq?0tFLE0SXNl2}O@tTa|$-m4Tk!QKx{49vC8&dyFxPyhaf@%QBK>G8?mKPP{Wj{hDWpB(%- z**`kjJ32l*Jp6rl{Oj;|_u$XY!5`xOAL8Dh?Y*O|-$$Fjk2ZcCuKzmR+1Xj!JtPu| ztGfrQI|nN}2g}5RCF1_#_Wr{5{`}V7+~(fw#vWng_ssh5>Gj`JYriJfe*Ij9u{*J{ zJHE0zw!AyKyfZ&PKeDtlytwmYkvOzK{60?{oZlXp+wPy+?wj4}o!#moz}W1b+3cFx z?3~{0nA&Kc+Q3h4v`wzJ{akPTx!y9d{%vBdd3+5TUuzm$ZGA6P6KSS;;d_}sTp(l`I9cfPo1{$tO4QTN;j z7+rJkyJibJXA3%K^E+nqItcID3Ar%vGdcK~?6#S#*6GaF>5P`?w=L7@-=^L)Po*_a zzJ?}Kp~;k{$>gS=NsT{WeVs^bm`G@th_9cBs~?Z88;^leI~H9#7F9DASu^^wdNiVX zG`wmwtZF3e%Sh;#kr$Q2&nt$XRSZ8Z9|p^Rgp~aVE**MOIu!JIDDd<5$0gqbO1}Gl zf-&e_9x+hLX7aXY=;@=d!% zTKlau{LR<+8!%Gw=BaIFDQ%`HZP$}qO_E!UlUj{nylOFg)nbs?qM!IpFX5YR{I_fI z-*n=dLqbBdW1F>NA90)ubBLq!QVv9NDP!vQhEnSA~eLSHr)`hc(EB zHCzd8kPWSuc~LL@qF(Abj5^6@brR3&uup5npVo?jwW1+4A~1q$goA4?KdBaaQY{!% zeJQ9)Ah3!b#^Wl!$6t5@zVHNm;r6e*=wHd@R|!85B7-vUGs&5$tEp!0Kd~|9q|bOA z!0$L3ND2S^uzHh^($rk~r72yardC9yu8BPzC8Ju@R|<`Y_=pImmkO{C7)8 zz~ZTE3n&BZM3ggSWv%A%yKv^fM7vCEsd1wsGn_w z?WuqYuCO9;y0`;_l$BU2T?MhvV;{flQoruN00VKL(wU{m=xc23C1|0=4 z|16bSBTu^2>q;0W4r}E2axB_;=9+JkarqWa&F84SPH-ArV$HfSpXX@4sZ0weHR@wuh>^PF)W`|znzp^P1x&o=^SvxXz;T? z8PVA_REvC$4U?!;i|goIw=YC7iVZNieg?m7$B-H8GF()UeIxp8*Xp^w< zx#tc5jR2{Qb_WP@ssZfvkNu;sHa^J7v4LL^TN}6`JyirN|NW`tIKGgnDJdGT4H5hG znsOQ?*@MIy!?qdSs+mb;1UQW7`@q+aS4F(x4b~Z_P3#?&YDbhRLY2lY{Ui+8LIvAj zN40h}W+yN}75NJL>g>9f2^jV|aK%Hsv(D>dE-=PLcwt&D!bvqi@v{BnAKj76+e<*i z3*gnm@$_{#1e|U#jjpmv0x6I{M_7^#)+J{kJn%B@Z{Qz4_)QD~5-#Ap5tcHvRPISy zpKE_yD?e82GXN6k3DH#LSSBiW^EOwJKYF{%pYE6enx^1~n2jD313{3r=~G^^y>sV? z9FV|>Gh$v(nWwGyKArGlhdf}y-rrl)w`*Zf^C~${={0=QRkwuQGI8<(ce|j@HHNUa z`iZ=qjO8z0pEu*FO?Mtl?FkxpIaWS+p0)5`?DyZ`{Z9wSM_dh$l|-N}VcES0=lv_} zOm66gA%1T^GxI11gg-tu>ti61XM6QEF&a5`B9N2MX$dBq4aqa>gXGS2@`-3txreJ;*5>dX1Cp1);I|_}Nsl72nLv^(;Uu zO^Tk6g}QW}m#R>gDZBp0YW*U=iM!`L4qX(IoogY$&k`)T`RIW*(4itNtDUG1=!aZ> zD7O3P*{{9Rbi<_2QGNID>?ahYl3{=N(`ZTu9W2(9$yRQExs-WG(l9acIg)P(ANsGk?dDrUeb{eN{}1CSU0hox5uV2bUywrbkgzUtYnhE6ItALAuLnGi9@V(eFmp zl+}D{bi4}5`Uzz&caTK;8;uShJWbU1db6rifMn0qagDS`Xv-Pz;@Ju7;~)0+^zS2o zmxL9QT7n6^SKI@|&MUrQ{Wv4ViVG6RJBC#y&mx{|Kczj^xp9jn4UH8F#}&cSlH2of zK_t@~?AA9N9I=oPnc@~7HTGVcvGN@X#hJHr!(38K*&$cnM!zD%n-iVDRUD2iX_!uez zl`j9bIG_+zR{2=gmHHB}Gln>uL?G<)=BrZ@K{=TZC0!X$0R`oPf02cKd8pyrM7B#C z_whrdJty?zqP%W7&|(=gNWKgD1%@BpXf{Bu=!f_C!`_5lzm@ev>yQ3EiTs{Rj#FnU z-svJi-=ZnZ5+=!fw6b~qH z{oXx&&NeqI>{T0+OxILD3#S?Pi?4X*1+c%pB@@;ze;59e8v`$ee^Mp-xHg!~n zA${OHAzP$)Tj(3`@lsX)uQb3@r9s4UgZBYg@U?vASdgeeFL6b+9qwNt4u1m5r>}~u zx$rjt%*k6!X;$V3&V;72Ts{c$_NBtE&iVNv#!fuXr|yDg5%GdkYVpu4Tqu-Cwp1*Ng-2(QmQkWg!$XwfR>@PzAOOO z{6uH`i*gjw;4dRTuM#ZAd9(gy15Ughoy%mX&lpDCgsXlhKPMV{a7)Any0CzAZmzBZ zzK)h)=Jnv7M?&b*qK2>dPbd@O_^?kKqL>PMWQKcYjKK}J?q!uys_rL6IY&xaFyNM( z;zMF6kj0UQUNaL2+)ro6^`NRB+K6*~TH3d4!5i^GtK_wSY{S5<&5HagO04V5yM8p* ze(&>f`FW_Q(5P}TwYQ7JH!lC$7g1 z>cPYEME}`^J3y5IyNQN%a|5t$NS+zXh{9NB2&;6Gm-?P`O3X18kc~aso7Tx#+~1zfTP)$-1r4btLf|7K(yI#lc-{+yBrnd@V4wXqF;%0N$J zeN0W95Tc+@qmx1dgR{h!*`<&Org`XldGJe-C_WLEJ)omQ`BQA`p7jDIa(izc&)poP z9q{_h@QX27tO#2bfTnnV%A<=1vo`#xFx2-yrW#f9XMl}!8#;OC?g8?Y4PxmJ-s zL9+1`p@8ZNt}A@$3E~H7MMw!}9{xopNl3KfpN7(yjl!Te4^S)er#p;5WZ~3+W=9@C zKSMShnF=fpX>J-w6e?CQsh(cCH9{vT3hnEgct>%Qe^)Z}?lZT8T6+QCuU@vf@h=gX z)%0mf3pm)#x3ml5{(+Z3(KCgiO&i&C4qQ(NxXsqxoMcSDDmC0VH?Ih+XDSUV^`|_f zESzM_0j=OIUe_RGaZLF}Ogh1}m#>}I~cmbbClqLfU1fHo!Q5a+u%`ecM0b09)= zh%|>c$qfaw_4;5GIHT=%@5!Lm%7D%A@Wv&A+GlfZGjO)4s9SEWkqp4D$KF)GOrjXT zTb0G1?4=57SD%THn<)Op7pPw)k=_!-e;YBC|4F{GsT52Do)~}Dqx*gV_nm>z>reUg zE3()F-<=L@ya~FV7(a|aO$$xiQc}6e@-b9Vx}SL?gkF^cE`dH5vqW#u5pusz?Q;eo zM|@HT#sr|hLu9Qk+Dl%A^6UJT$wO>7g5wJhM-9q^MmsV;A zQkP7}MM6S69)mMXqZ_i}A&Nz!~^RQ%V zxR!8~Bp@y;HCdHN>@o)&=C&}t%`NwOuL27OIhl1eH8q=)QW&3ly>@3QxclzOmX^G? zI3E&CiL?jNrKae3grq8}17NxM@A^L%KN{j;orVw-qz9+e|BsF31VAOjyShE?cT2Ek=gt9T&uK%300GrR8JAmi+z&Z(46b6l~ zq%Pq`q9YM&csS($wR5Az)+vTHK8-b>W2wQ4>D0t2-@ZzEy#0E$$Yc&{a)Hc6sr+Qw z#3$_D-QT#1)SQsqQe-zL)v8?%WaJ;PI@*D2g&wk~_ra`=UUp!GHW#%0sHti1}?$PbQ7T{_WxlEq7t_Oa_;__wHmV zlHXY1>K`-r6Dq|P zKaG2%@Yk(b;ulejOWWlADIacAzApqbYvcmY+3=2aJ3mwX^Ib>%8GaFTmvM*M0Va?-TyjNgd@@IEOa`?Rz-I&~%dw-JyUKGE6(u5O@*1v+F zU#nR?d!}P+Mujxb-IK<3>X!Uz|9y@O)008x9l?HnouNISfMc>og%D-2WCt>Rl%I}!q@n0K znO)92kgkw-dt>qol)iVTR-?#`6(Z zWYajxJl`$-gsVfYrEQy1qTpUZIO29|;4dSFI79beQ4dk?vXL`M_6hGCkxkwnd+CX$ zvbNGT!wxZrWo=^t6{Az1*B;X}wM(e#|F$7(nCV&Ej$EfcqYSdlVPtJW7IUubr!wNt zVs?V83dj^k-E=yKUY3MdEzw(Kv;w`Ot2(&pvY`(>#`rs{q8NHWR@)oUz6w@1O_x zJl^dlT#+=w5Q3-vuJED<*p<$Jf-g+A;BnjE*ju59H@W-zP=P)Td`7{<7z&{W(0G0} z@THF?hJd)rM7RKlb?xs3iwS*SI}nk{B{7Tx2bmRkn8^-?pkyj6(L?v2Wb6y6BLye z@|J!HCN25!8*rs1{x?%;y@Dd0>m45bau3Bq6h+5ZVY1@R@$>`nXagw>ZUJ-a(bKcq z+ZV3-k;{?Reu6_VsAqw`(dX$+vg77AYv40(-jABkK&@Z9UUJP<$Zy9Pjf_rul|o|k zC9%lU19W^RcL#&B|B*)F($E_JPa-pVuNVtOV8y0L4c?3Dt)hvR#SxAm-QZ(ZqK@%( zeFyN;ACDq9ng}+Sv95dMrs4r-k!E3dKC;{I;U2RM8rXpcc6J;nizl7pK)U#?KeHMh zpbpdAJzH?t;U8u3Yb6aaXFD<}!?)m*Z$y@oA+okgO%&L82xond?``#{H6vv-ReTV# zGcT>+RKgf0EkBOdbCqNdf+DjT(51H@#va)HJio}LT#54Mrc$6p7Nh1Hd?2>HHi0BE zi$EkN1ImI%R2gyeH{DMYCW5BOr)HpK`KuC}R@`B$vNxGO)qoiWH}*Y*?7q(hh3kyj z+v1AigS4fUXh+i5hu)OF*4uPel*KjAtoAV@!}q{h)*4p-!m_dG0-&D3E*=XJVG6VG z2^)*e1?-mQlx#uUo#_VpHaq&(UrOv{2kDW9AC4&fS$2O$68)f} zhl(r~N+vG)-P_<^6)q18742F#aqt^oihSZkNfZ@{b^vb-uj&v+z00Kd5&Js2SH{Cb zBB_(z4Cu<;;^?e~b_#1+(Tbwz(uaSF0c9s{*+O2ZFd?A>_?UN^4o!GH+WqliAE-~x zZo)#Bv0*NuAwh;^Dl`bZSgRGBa%Z<3K+jy~NLfkPUyANwa4SKy3>TM;(nAhS7;yDL z)&g*nc}X1Ec#Scy~nw3q)R!n zB@_`>u<4ESp!K-WjSQmovcur6&29DSOe>Ym3Em|4KNnd~fkbQd^Fx&*b66&F3ug{a zdKBi_1U+c@Jmc4_hA!AYFV2N5ek4X&wU?k5c+{H=`C4tG5N0r z(WR1Qyq?#w6X(+Zj*CL%WjV6Q;vP!sr!1STImPHwGj1_>!<60bKPp?)TYhqAaz6XO zG3Xu^qGDmTC;2?gxLl5ftV%0tu~1~?3A;sbq+FU0qG~KOy@%y8bQgg~_p8ln_)cty zH!xN&k>VX+FWQ2PD_6N*k6&l+D+k22XA`~w&-GYuJ=+|eaH&vXBeQVi-C{;zeq_r) zd0}%wH^~mEQhwmTW|;CU#*HVTT)_BwQ#cmd4G7;RrQUWL%_ouul7L6U=Nv4-!q242E@E4DkHNj_InJ*dmJGO->ZdZ#TVo7p z-7he;u5olF0!pKgbUJJJJoqb+>|kO^WOz9b#2W%E`7x$S%0uv$%nuO!h5W5#9kDTc zZhXTy1e`CutoF;KfZTu0lTeW|cXtWp-@x_PoYuh;^synA?DyMhYH*^)x^hnsC?$PB zu>NDf@!N9`3s7=Y>FaBpT|L0h5NZ)cHS0dO$4oF?)JS8-#T-+`2UfSp9uIM3F9!xW z337E+0)e7rjov**^ZOLU#|wFpE1X?WlBR!1*%C1=-^_!WbX=greS#Ng}5%S zA@(iM25!7?9Yy?Jtub5ubK%6Zt2gok-@ivaux>yc+C;{$!X=AV z(sBJGlR-3gkDAawt1JJRUWZL<%y$|STYo#IoVk?J-4maPa zinmlxAs@R+d#gLgoA$~7v`jg=a$;9S;C5ao`wPWw<$P28 zet?7X68CMR`9CBZTRJvEsjyUGH^~7qcik@(glcAX-Xy_0V3s{?_j2i}`e}E10ia`N z7MWyxU~)s{d(Azmlm1K8LR6)82$cSLIGo)C_`n!dPwhlC@C(_9%?s)ZFPcm6I9TG{ z=7hJ32*egYW%_JT$$U6=qn7mbh~BkZU~f6r;MsR2^Bb|nN{BJJ*YL{}e!oVK-=)+( zGRfg`LQhjPSnFJ~0+-*tuOKZ9d>9sK>fm-n5Ke312b~?7A7@d6iV1@oeB@ysm7%sKfe? zT}K#ASqK<=0sT;TrB)L}RmfAac=Dv6cTw({=J3)dv$J^VoTK7exF2#G`Ma+<+*D#w z<3`YeM-xIyfy#-7C~Gg1thd5hl*{`HpNjh_Gr3{x27VaFTuz2>m`o?Lg3145WBddV zBQ7z-n{1+NKERtELR!3WDNhb+7F@ZQ3{OHl@7EUJoR#arhbA1jsot7fm-B7kf0w*0 zFZo0ayDrF0O@jWL@-?>2;{wS~ve!g=319SLIPD^0|M{6eW#|-%q7>4<`epnUK!4M= z;l;Q62-zpJ9hi%GO3bB zHJp9C5!WZ7ejACNEbrc(P8nFCDQ(CCJ=w1uo3RrR)x|zfC=(`tGo{4+AO#(4z{9R9H!oP(8-|C*A*_%-=s=JKu?>!@wy&#-59Os+<_EC(K`w->Dp1=P-ND z$Kv)ojoVe7ahgBxI=-uH;kyXe805-)oH26kIU4fYSI~DIqir P0B*WkMw%t+xM%+bNnNe# diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout8.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout8.png index 99ba9f294a7a5447095ffd17a43f930a3842b5cc..5bcd4248fe2eb3cc74d60338816a724a7b9ccaf6 100644 GIT binary patch literal 9221 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 mKz_I2sruC>^}?R+JchD!{Xa5V*dKA9^ literal 6995 zcmb7Ic|4WdyI%Lb~0rfGj7RHlFT9V zHiQTv#Ktzyjl>DD`dm*t|Ku+D=ToRms^_H}sNLa5b*B*$q8o7F-?md24Nz zCVeP+3s3~bY7u^s!C8!s0YDi}sr@R}p1A*BeVGL{7}H2(VspFo+S=7s1b}F1R_r^A z+EJ^ZG?yU&WFMIm*|8<^$g=BR9zRWS_HvT1-H^YsadsFJ{2MMXdGzyZ@qN0O+g9iT|Uo|G=WBkUlR`nHiP8rMS7K9=wv?xJ+JM z;aMSnGD;eq>heUi2PKY19Z-7j=D4Ab5t5<#fo;zJ3)^4yS@>&XcbXR?7G0;X0bWA2 zR>5Y?i*rLs8!o~HlYN#iAB{eah+mm=9c^~?$R#5wumN2l_pqcvY?X@TwA-TJXB5v+)q>zBp1-wJ)o|0YWx>+dA)zBfaEsh8_Q45 z|H75OZQt?|T-8}kBuCtqjM2X^ctzAQtkl+6H{@r%!P-TYF57v#a*L}6tgQ~ zrJXY+16aLnb`TnY#92r1KhyaiX#O_NTF%;&3%8&)SQ#t%u)J(oZE(>2h(mfoL}{ac zwFXCfYILb>M{-zc!Hin_+6zPIF#z9`oOa|Pt(y-cPk(0%02exktFV2Z7fnt3FzihC zmc*b<^v3i0JR#%W?Heo8*ji3Tx}VcyBh>*M;{KBD)m*GQL8Q@=nwpXF`B|FnU?pyT zgtTLIoNigsBOb)wF9vV{ExgW;4&}0| zEr~qr=UrO#f9%_{3#5guTj;`L_RCvGJW$rqhz&a=czF4R##gZfDJ}E3M{Mkk-9G;B z<_l(nxvOVTEqKB;a!+T@(0v5XJY8cgesLX`d}R(=f-OGr7d-4$wf+D=^(g^Y!dg_V z75-{b>SR|zPjOb^R&}gMSE3o8OXjG&#XyA5>ASQb@`7X9i0Ts*J5qH#Oc8(jQ74}< zWvbpBXR2C1fFh{RNykSqeurLm2<*T31&7Y4+O2lC z{`1VJmuu+i=IY!+q>$4TKjP!p=SEKahhEgy?l$FY1ak9~2+{zM6ePP!d>bU4)Zlkt zKCf+cu5WR;s;^c(I(OEJ29#^>$i7dqH~%hU=yA{INhg`KI34oM{P8$b-I`xt%bYSN zJp%yR=Nc^_7GYhvG&2?hyiqV3=4sMCvWlS$1x8KbZI@SVFdKL4b32~3 zq{WmL#Aiw;xFl#8!dUBSoe8JJE(%|bKHY<71VH%nCleU|^T|kKVQk>|L!>oz!}jFl z)<7b(oBne6sOH!G(W3uEuhdd6;cr_YFDxE>#& z$$8CqeZP|T(}Ud|D>)(LAjQ5^h1TpE|I`kx-Pd3AF75?snb!q6jp3)nMimTF1?T~o zCg2PenF09SK+Di%eQKJJEsLT9pf_Dg*Cp{aN%hTJNJ25*sw&94!_NpI`9CwBvD4@h z&d7;RzH&nGXVxFr)xH`eL9@!0Lg+x3q#L^o(Sf>yXYQ;Dlra`Ie`IDDlJ=aiZOREFkoW#LN?;q)*Ov9*YzYKKP_Odqew^Ed6<1Qn!|xasYxk5(y0S zn&YEf|KL1Sc=xdx{RAh1a)0Tg{#XG@kl5uHUMbCOs({kOe1!#7A~LLYKmELNaaDVjN_=lkhb=~ohRe5#)#5= zj$D!9h+JWrC@zwx9}F!)Ij7B?!W)i@%mEU1ak&zo=V$>!DOjNT1sm;!k4DJqLlfL) zYQGMPM_jwiLjTPx*D0%~!GT4l|FYacQ!z;i<~`z)%v;p&rs!DOyiLYr#8!W2O9~4= zp47yb#CBd4%W9t;VBYv92YcpazTKCB0xY`1s@8usSc!>60WGq6bJJ^+7~nR+N$T$I zj?Mq<$==6+#JY&SQAJwUtETLdu+82T1V7DYVxjkH-7ReoNmxw#=e&rbaN%*nZ^)=~ zIiXF0MZRaKr0;cg)?B*&PEz4$HMt^C!EcApD#ULwJ#c%eX(e!*T%Wz+xbrQzW@L9B zw}QfLDO}+RISAYd8F88!agVN7utMo<7X=K_7N}>jV@T8#1WQbOn32$>DV4Rp)P|j` zg*TBFSr!&$B-av&z{=<%t^JV~Lu?VE2G$4SjP|g!A^`?Bkyri6g2+!Xi<@NKi@7}w z+SDK|BJSo`upKw{dZl@4N=k!vvz^_mjd#@=KtrHgN1!aOy*k?`u~9|jXv9i>mctJP(PY3y zn$V^K5BM`b;99q}M69VnXvz<}pMIy+S!F@?L3+phoBMFX9=LfC==K+D6LrKr@EVo^ z73k=;srC#pL9X++r^xvRPA~YIWD!&Q9k_4FB|g?wdy{09aAB~cHsqKCDMl;fvDKDf z*Um~HmqSR3+DJtW`Ibh2`;tmVzWzwFqThF6Fy^i)5UU~Jsz(b#4;7tb0bp&^+YLU6 zYhsC_mc?fIF@)Kup)@ht>BXw1mRM0`}1Lwm*12(|CFh5B%-{j0Rh z6~B@Y=Ibs{ytzo|OF|gT`6Lm~nJxjqhuHEgL5d$EACvoYM!2D5Z=Ys zcln*6z{86z=QjkmLYQ)UX2(xbfe&h0H>l9W z_?FKD1DBDyrz!BAUy?zx$1nk@qBT8Ix(F%{jlSq}oHc&vVf{C6(k5KJ1ObTc?M*yQ z*;LYcUGNy8pqD9QmWPp}RC|vZEeliOgkoY6Qe(z?Z;@XL#lvhAD=K%zV-#bL6Gp?0 z_}q5n^gHZNt|><`J2Z^$r#h~tb?$cDBlfGc{$u0g&sG@u;OEw3?Ek$2d`R-iPq-iU zGv2S>JV&J(|#Z;Y}6b)%bR&ky}w&> zfu56CSo2}=)ry`NI*H`#>B$V-l{4b%A7~0B+L;|CNXPZ5?Va149aoNckzvSl-cLrl@D!gk?rbAo8pi^j&WaYIf;LCD zH%W`zQ^RPiOX@&uVat>W0Q`>TNyf0g@H<*KVHp-GzO1jWccx!R>UjSG-)4_ABioo3`A+s#!?Zd`K6GDFx6`7a&o&TAK7A`dH|*1^1)Ho_f{Ljo8bNvRt>!|6@?r0t9zRC!cgpoZy)z~X~AeKxoD`} zx9orqn3DBih16<_cI?OtTi==pRBTj$r=f^SlT{tnuOu=lX<~_u1dq(GLc3?adw9um zj`Jwi?E#?W%jK)P+pmXWd|SiGn;0kLB>&gACm!Y}lweCRu`E9OSUmU1wMN8wK6$2( z-EgyPEfV}4N!+nnq3oPqReHEok9Y@paRr{Htby&FDpR{-cqBzqPng^tgAFHJ-KA3t z$U!bN3a?q71#QU_EJ?J56#?FZd@ypM?D8&;d$@Cv0`=`F7xoj=H!aL>d%>m&M)v^J zfuZQ&K*Nvd#yk=6k!jh3P;Z`4?w~C+u@}|Tiiw9=^8^Y&ZnqM;O>AFHFx6$GiuVT8 zgF?5wT@>=7P9245do}zBTEeJ~#JI??21bwO8@^R009*1*`p8|X^%ie)xoWDCIr9wD zv7lA{efd>k5g5Z~276uF9pn;jZ2WbMPA#gVzRLZR#O?U{_!TNpN6ThOlZ%7-*HP6YWtcZu2F#aEk^A*0jq_yOngU2akwY3CH*0{2^{-$fR>XQcRQ&cyEq15uWo{oF_97t`X6KM1w@ zDK_))-^jixQ$94GO(yf6lsrI?de@j^ctp)=bP;Lu-8Y*pkKBP(K#Y#Ql1YN6y9H)G zK}+ECm*QARQFohjWq&f0q0wmsA!IZ!N$v_APqk5k{ckjX#eAqN>_}mU)PH80N^)TU zQsL{^1m!Gn1UvG(y`N4}vA4jERG0nocJBAejpfUmI>Ex39c_iUuIclm)~7TlPgqd8 z=a`n4Ov}=UVSn0OhcAlh?_(iqZNV)PHh%TXMnlWoV8= zVw)s#ueba8VnP_wd{w)48?ZHgioJN7u`*P+(m-D`i%2ERScI9Jpgt({2Q@Dpoy1!W z0WPhBE*cy|stR}$0aWZ?4uOf&oo?T0w^o~ru|6I=O!scRK8_qd_nUA8qR{%I9v#Kb z{x0Xa5r6wr@WOrN*{q~&Q^Y=lX?O$T+&+DLKWQ-Gj8+4`{cmTSOfqd6FBBdj%LQij zkCF;moFWwk)+YO~4NM~K-tHc*ghIo7B()4o?Jnmn8ZsdWYG|P1P=+F8yaDdx(v@nb|1%=R9#U#{KvXieW2oj3O^Gi*saK2mrhF2vtOP+?VL93 zime)U&+nUCC2tqKfw5Y+oZE9OO1-;n(oeCPx23qK8P1mf<#RHl9{yssM(D0gjX6(@ zDDEnbUAbc;sx)OLEns$k+n1#FqJ!t%{Wd*47WrVZ`irdwuLdT4FRq;(c5Z_*U3ew) zfUW)?A&k)4&`<3lYx7*0H^lbUe`6QF)E{Qe`$VLUYp;#{Ny5>HxSV1hCwUH)uc~jQ z!(<{^UmZMR^VD#!>TABY$&-HTYht4I>r_C)nt$_1Epn0?vxizJ_oBiuJ5RtX)ZZ{ofB3dc;TMYl`vd;K4GUJ`PiPXw(G9}`Fw4qBn zudCwt-u%{^nz>@-8q}XUF!$S;n@p;lvR*XN#7NutaeglOe3~;)dp$KX_R=1Do%)^po^F}Y z6e9YGnt=jaT2x5S$DY}+6s*Tbx`z5;pM&BKkr9@w>qm|(x>*>Uzv6w2NF1SWnE870 z^vfB$udfRHqQsY%kyG)We;I}!82aR0c0CF}9oM;Ck@S??sjPmR*+L=W8kya$NM{2#Px%F4{|BKgD|h$v_Py z{m029GYzMFd_kWR75JumF~6sfbh_1Xf=lhn`x$S6FN%oteZ`&6CI* z0~)qf=H)GRKZkTz)BQ+$luCh&V!BDdlT<50BPP9g80b9tA?oXVAl%OXZ*EbtK?(sk w;NL+JnvJgrRAvm-Uxu~xKVtR23awMhU^<@rP3HL+lCVHiO;5Gp59^!%1&uq%RsaA1 diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_submerged_with_colorbar.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_submerged_with_colorbar.png new file mode 100644 index 0000000000000000000000000000000000000000..f53c2b8035a94975fa1ce1efb66a2a9b96101587 GIT binary patch literal 6486 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 M-$d{3KEkj63shQ!D*ylh literal 0 HcmV?d00001 diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index ff757c1ce9fc..7d1314ed5042 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -809,3 +809,47 @@ def test_submerged_subfig(): for ax in axs[1:]: assert np.allclose(ax.get_position().bounds[-1], axs[0].get_position().bounds[-1], atol=1e-6) + + +def test_submerged_height_gap(): + """Test that the gap between rows does not depend on the number of columns.""" + + mosaic1 = "AC;BC" + mosaic2 = "ACDE;BCDE" + + fig1, ax_dict1 = plt.subplot_mosaic(mosaic1, layout='constrained') + fig2, ax_dict2 = plt.subplot_mosaic(mosaic2, layout='constrained') + for fig in fig1, fig2: + fig.get_layout_engine().set(h_pad=0.2) + fig.draw_without_rendering() + + for label in 'A', 'B': + np.testing.assert_allclose(ax_dict1[label].get_position().bounds[-1], + ax_dict2[label].get_position().bounds[-1]) + + +def test_submerged_width_gap(): + """Test that the gap between columns does not depend on the number of rows.""" + + mosaic1 = "AB;CC" + mosaic2 = "AB;CC;DD" + + fig1, ax_dict1 = plt.subplot_mosaic(mosaic1, layout='constrained') + fig2, ax_dict2 = plt.subplot_mosaic(mosaic2, layout='constrained') + for fig in fig1, fig2: + fig.get_layout_engine().set(w_pad=0.2) + fig.draw_without_rendering() + + for label in 'A', 'B': + np.testing.assert_allclose(ax_dict1[label].get_position().bounds[-2], + ax_dict2[label].get_position().bounds[-2]) + + +@image_comparison(['test_submerged_with_colorbar.png'], style='mpl20') +def test_submerged_with_colorbar(): + mosaic = "AABBCC;DDDEEE" + + fig, ax_dict = plt.subplot_mosaic(mosaic, layout='constrained') + + cf = ax_dict['A'].contourf([[0, 1], [2, 3]]) + fig.colorbar(cf) From 5310f9754b5323421da262e882b312e11699f00a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 11 May 2026 19:12:35 -0400 Subject: [PATCH 047/215] ci: Re-arrange AppVeyor pipeline Move the extra packages into the initial install step, and drop the conditional, since `TEST_ALL` is always on. Also, put the build step into the actual build phase of the pipeline. --- .appveyor.yml | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 13705adc99f9..4521bc876a8f 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -29,16 +29,12 @@ environment: matrix: - PYTHON_VERSION: "3.11" - TEST_ALL: "yes" # We always use a 64-bit machine, but can build x86 distributions # with the PYTHON_ARCH variable platform: - x64 -# all our python builds have to happen in tests_script... -build: false - cache: - '%LOCALAPPDATA%\pip\Cache' - '%USERPROFILE%\.cache\matplotlib' @@ -57,10 +53,18 @@ init: - micromamba info install: - - micromamba env create -f environment.yml python=%PYTHON_VERSION% pywin32 + - set EXTRA_PACKAGES=pywin32 codecov + # These are optional dependencies so that we don't skip so many tests... + - set EXTRA_PACKAGES=%EXTRA_PACKAGES% ffmpeg inkscape + # miktex is available on conda, but seems to fail with permission errors. + # missing packages on conda-forge for imagemagick + # This install sometimes failed randomly :-( + # - choco install imagemagick + + - micromamba env create -f environment.yml python=%PYTHON_VERSION% %EXTRA_PACKAGES% - micromamba activate mpl-dev -test_script: +build_script: # Now build the thing.. - set LINK=/LIBPATH:%cd%\lib - pip install -v --no-build-isolation --editable .[dev] @@ -68,13 +72,7 @@ test_script: - set "DUMPBIN=%VS140COMNTOOLS%\..\..\VC\bin\dumpbin.exe" - '"%DUMPBIN%" /DEPENDENTS lib\matplotlib\ft2font*.pyd | findstr freetype.*.dll && exit /b 1 || exit /b 0' - # this are optional dependencies so that we don't skip so many tests... - - if x%TEST_ALL% == xyes micromamba install -q ffmpeg inkscape - # miktex is available on conda, but seems to fail with permission errors. - # missing packages on conda-forge for imagemagick - # This install sometimes failed randomly :-( - # - choco install imagemagick - +test_script: # Test import of tkagg backend - python -c "import matplotlib as m; m.use('tkagg'); @@ -90,7 +88,6 @@ artifacts: type: Zip on_finish: - - micromamba install codecov - codecov -e PYTHON_VERSION PLATFORM -n "%PYTHON_VERSION% Windows" on_failure: From d053d9596936ae21d5bff8fabb15d22ef3a0c53a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 11 May 2026 11:28:01 -0400 Subject: [PATCH 048/215] STY: add shellcheck --- .pre-commit-config.yaml | 4 ++++ tools/create_DejaVuDisplay.sh | 13 ++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4602570328a9..5a70c32cc3aa 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -78,6 +78,10 @@ repos: hooks: - id: yamllint args: ["--strict", "--config-file=.yamllint.yml"] + - repo: https://github.com/shellcheck-py/shellcheck-py + rev: 745eface02aef23e168a8afb6b5737818efbea95 # frozen: v0.11.0.1 + hooks: + - id: shellcheck - repo: https://github.com/python-jsonschema/check-jsonschema rev: 13614ab716a3113145f1294ed259d9fbe5678ff3 # frozen: 0.37.1 hooks: diff --git a/tools/create_DejaVuDisplay.sh b/tools/create_DejaVuDisplay.sh index 8ee3b1659e62..3db4015b887e 100755 --- a/tools/create_DejaVuDisplay.sh +++ b/tools/create_DejaVuDisplay.sh @@ -11,11 +11,10 @@ # generate the new font files `DejaVuSansDisplay.ttf` and # `DejaVuSerifDisplay.ttf`: -mpldir=$(dirname $0)/../ +mpldir=$(dirname "$0")/../ # test that fontforge is installed -python -c 'import fontforge' 2> /dev/null -if [ $? != 0 ]; then +if ! python -c 'import fontforge' 2> /dev/null; then echo "The python installation at $(which python) does not have fontforge" echo "installed. Please install it before using subset.py." exit 1 @@ -23,7 +22,7 @@ fi FONTDIR=$mpldir/lib/matplotlib/mpl-data/fonts/ttf/ -python $mpldir/tools/subset.py --move-display --subset=dejavu-ext $FONTDIR/DejaVuSans.ttf \ - $FONTDIR/DejaVuSansDisplay.ttf -python $mpldir/tools/subset.py --move-display --subset=dejavu-ext $FONTDIR/DejaVuSerif.ttf \ - $FONTDIR/DejaVuSerifDisplay.ttf +python "$mpldir"/tools/subset.py --move-display --subset=dejavu-ext "$FONTDIR"/DejaVuSans.ttf \ + "$FONTDIR"/DejaVuSansDisplay.ttf +python "$mpldir"/tools/subset.py --move-display --subset=dejavu-ext "$FONTDIR"/DejaVuSerif.ttf \ + "$FONTDIR"/DejaVuSerifDisplay.ttf From 76e84f792453787760d150f16395d4880d2c9f7a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 11 May 2026 14:14:00 -0400 Subject: [PATCH 049/215] MNT: fix warnings identified by clang-tidy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These were false positives, but add code to give the analyzer a hint + asserts in a debugging build rather than NOLINT to be defensive. Aided by 🤖 --- src/_path.h | 9 +++++---- src/mplutils.h | 10 ++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/_path.h b/src/_path.h index d74d5c0d8ba2..7ee3ba405979 100644 --- a/src/_path.h +++ b/src/_path.h @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -115,6 +116,7 @@ void point_in_path_impl(PointArray &points, PathIterator &path, ResultArray &ins bool all_done; size_t n = safe_first_shape(points); + assert(safe_first_shape(inside_flag) >= n); std::vector yflag0(n); std::vector subpath_flag(n); @@ -242,6 +244,7 @@ inline void points_in_path(PointArray &points, agg::trans_affine &trans, ResultArray &result) { + assert(safe_first_shape(result) >= safe_first_shape(points)); for (auto i = 0; i < safe_first_shape(points); ++i) { result[i] = false; } @@ -270,8 +273,7 @@ inline bool point_in_path( *points_arr.mutable_data(0, 1) = y; auto points = points_arr.mutable_unchecked<2>(); - int result[1]; - result[0] = 0; + std::array result{}; points_in_path(points, r, path, trans, result); @@ -288,8 +290,7 @@ inline bool point_on_path( *points_arr.mutable_data(0, 1) = y; auto points = points_arr.mutable_unchecked<2>(); - int result[1]; - result[0] = 0; + std::array result{}; auto trans_path = agg::conv_transform{path, trans}; auto nan_removed_path = PathNanRemover{trans_path, true, path.has_codes()}; diff --git a/src/mplutils.h b/src/mplutils.h index 95d9a2d9eb90..475530b3d880 100644 --- a/src/mplutils.h +++ b/src/mplutils.h @@ -53,6 +53,8 @@ enum { // so that we don't need to access the NPY_INTP_FMT macro here. #include #include +#include +#include namespace py = pybind11; using namespace pybind11::literals; @@ -113,6 +115,14 @@ safe_first_shape(const py::detail::unchecked_reference &a) return a.shape(0); } } + +template +constexpr std::size_t +safe_first_shape(const std::array &) +{ + return N; +} + #endif #endif From b8fb1b4bc5c37f08d47c2d286b5b2df066fe6e1b Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 11 May 2026 14:15:26 -0400 Subject: [PATCH 050/215] BLD: add NOLINT directives to _qhull_wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When for no-change to code with the qhull wrapper rather than adjusting the code. Aided by 🤖 --- src/_qhull_wrapper.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/_qhull_wrapper.cpp b/src/_qhull_wrapper.cpp index f8a3103b65f1..509b48ecb3e1 100644 --- a/src/_qhull_wrapper.cpp +++ b/src/_qhull_wrapper.cpp @@ -54,7 +54,9 @@ static void get_facet_vertices(qhT* qh, const facetT* facet, int indices[3]) { vertexT *vertex, **vertexp; - FOREACHvertex_(facet->vertices) { + // After qh_triangulate() every non-upperdelaunay facet is a triangle, so + // this macro iterates exactly 3 times; the analyzer cannot prove this. + FOREACHvertex_(facet->vertices) { // NOLINT(clang-analyzer-security.ArrayBound) *indices++ = qh_pointid(qh, vertex->point); } } @@ -66,7 +68,9 @@ get_facet_neighbours(const facetT* facet, std::vector& tri_indices, int indices[3]) { facetT *neighbor, **neighborp; - FOREACHneighbor_(facet) { + // After qh_triangulate() every non-upperdelaunay facet is a triangle with + // exactly 3 neighbors; the analyzer cannot prove this. + FOREACHneighbor_(facet) { // NOLINT(clang-analyzer-security.ArrayBound) *indices++ = (neighbor->upperdelaunay ? -1 : tri_indices[neighbor->id]); } } @@ -229,7 +233,11 @@ delaunay_impl(py::ssize_t npoints, const double* x, const double* y, int indices[3]; tri_indices[facet->id] = i++; get_facet_vertices(qh, facet, indices); - *triangles_ptr++ = (facet->toporient ? indices[0] : indices[2]); + // The analyzer takes a path where FOREACHvertex_ executes zero + // times, leaving indices[] uninitialized. After qh_triangulate() + // every non-upperdelaunay facet is a triangle with exactly 3 + // vertices, so this cannot happen. + *triangles_ptr++ = (facet->toporient ? indices[0] : indices[2]); // NOLINT(clang-analyzer-core.uninitialized.Assign) *triangles_ptr++ = indices[1]; *triangles_ptr++ = (facet->toporient ? indices[2] : indices[0]); } From 99ccdca3f62dc9ec163c77c9f86a112dfcdece08 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 11 May 2026 14:29:32 -0400 Subject: [PATCH 051/215] MNT: ensure that variables are always initialized MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes an issue found by clang-tidy Aided by 🤖 --- src/ft2font.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ft2font.cpp b/src/ft2font.cpp index e853346bf1f4..bbd2144d827b 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -527,7 +527,7 @@ void FT2Font::load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool ft_object->load_char(charcode, flags, throwaway, false); } else if (fallback) { FT_UInt final_glyph_index; - FT_Error charcode_error, glyph_error; + FT_Error charcode_error = FT_Err_Ok, glyph_error = FT_Err_Ok; FT2Font *ft_object_with_glyph = this; bool was_found = load_char_with_fallback(ft_object_with_glyph, final_glyph_index, glyphs, char_to_font, @@ -595,6 +595,8 @@ bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph, FT_Error &glyph_error, std::set &glyph_seen_fonts) { + charcode_error = FT_Err_Ok; + glyph_error = FT_Err_Ok; FT_UInt glyph_index = FT_Get_Char_Index(face, charcode); if (!warn_if_used) { glyph_seen_fonts.insert(face->family_name); From 419087b47731e1df877050b9bea65c6bebdc007f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 12 May 2026 12:55:51 -0400 Subject: [PATCH 052/215] BLD: skip two more clang-tidy warnings in _tri.cpp --- src/tri/_tri.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tri/_tri.cpp b/src/tri/_tri.cpp index b74fcbec26fc..f98d88d1adbb 100644 --- a/src/tri/_tri.cpp +++ b/src/tri/_tri.cpp @@ -1031,7 +1031,7 @@ int TriContourGenerator::get_exit_edge(int tri, case 1: return 2; case 2: return 0; case 3: return 2; - case 4: return 1; + case 4: return 1; // NOLINT(bugprone-branch-clone) case 5: return 1; case 6: return 0; case 7: return -1; @@ -1472,7 +1472,7 @@ TrapezoidMapTriFinder::initialize() _tree->assert_valid(false); // Randomly shuffle all edges other than first 2. - std::mt19937 rng(1234); + std::mt19937 rng(1234); // NOLINT(bugprone-random-generator-seed) std::shuffle(_edges.begin()+2, _edges.end(), rng); // Add edges, one at a time, to tree. From 4b3ce4d00ac33d300324d29d0dc24e785593c931 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 11 May 2026 15:00:01 -0400 Subject: [PATCH 053/215] BLD: add tooling to run clang-tidy on c/c++ code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplified at Elliott's suggestions Aided by 🤖 --- .clang-tidy | 36 ++++++++++++ .github/workflows/linting.yml | 24 ++++++++ doc/devel/coding_guide.rst | 38 +++++++++++++ src/_macosx.m | 66 +++++++++++++++------- tools/run_clang_tidy.py | 102 ++++++++++++++++++++++++++++++++++ 5 files changed, 247 insertions(+), 19 deletions(-) create mode 100644 .clang-tidy create mode 100644 tools/run_clang_tidy.py diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 000000000000..64263ec96b7b --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,36 @@ +--- +# clang-tidy configuration for matplotlib's src/ directory. +# +# Philosophy: enable checks that find real bugs (memory safety, undefined +# behaviour, security) and suppress checks that are high-noise style rules +# inappropriate for a C/C++ codebase that interfaces heavily with C APIs +# (CPython, FreeType, libagg) via pybind11. +# +# Run with: +# clang-tidy -p --config-file=src/.clang-tidy + +Checks: > + bugprone-*, + clang-analyzer-*, + objc-*, + performance-move-const-arg, + performance-move-constructor-init, + performance-no-automatic-move, + portability-*, + -bugprone-assignment-in-if-condition, + -bugprone-easily-swappable-parameters, + -bugprone-implicit-widening-of-multiplication-result, + -bugprone-macro-parentheses, + -bugprone-narrowing-conversions, + -bugprone-reserved-identifier, + -bugprone-throwing-static-initialization, + -clang-analyzer-optin.cplusplus.UninitializedObject, + -clang-analyzer-optin.performance.Padding, + +# Only report findings in matplotlib's own src/ headers, not in pybind11, +# Python.h, agg, or other vendored includes. +HeaderFilterRegex: '.*/matplotlib/src/.*' + +WarningsAsErrors: '' + +CheckOptions: [] diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 0d6e71198817..2a27ffc320af 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -84,6 +84,30 @@ jobs: -tee -reporter=github-check -filter-mode nofilter + clang-tidy: + name: clang-tidy (macOS / Objective-C) + runs-on: macos-latest # run on macOS so we can lint the objectiveC file + permissions: + contents: read + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up Python 3 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.14' + + - name: Install OS dependencies + run: brew install llvm + + - name: Install build dependencies + run: pip3 install meson ninja pybind11 setuptools-scm + + - name: Run clang-tidy + run: python tools/run_clang_tidy.py + eslint: name: eslint runs-on: ubuntu-latest diff --git a/doc/devel/coding_guide.rst b/doc/devel/coding_guide.rst index 7a4b296b52ce..b83f150e9c44 100644 --- a/doc/devel/coding_guide.rst +++ b/doc/devel/coding_guide.rst @@ -131,6 +131,44 @@ C/C++ extensions implement new features only if the required changes cannot be made elsewhere in the codebase. In particular, avoid making style fixes to it. +.. _clang-tidy: + +Static analysis with clang-tidy +-------------------------------- + +Matplotlib's C/C++ sources in :file:`src/` are checked with +`clang-tidy `__ in CI (see +:file:`.github/workflows/linting.yml`). The check +configuration lives in :file:`.clang-tidy`. + +The logic lives in :file:`tools/run_clang_tidy.py`. It requires +``clang-tidy`` on ``PATH`` and ``meson`` and ``pybind11`` installed:: + + pip install meson pybind11 + +On macOS, ``clang-tidy`` is not on ``PATH`` after a Homebrew install:: + + brew install llvm + export PATH=$(brew --prefix llvm)/bin:$PATH + +The script uses a dedicated ``build/clang-tidy/`` directory (created +automatically on first run) and delegates to meson's built-in +``clang-tidy`` target. To run locally: + +.. code-block:: bash + + python tools/run_clang_tidy.py + + +To suppress false-positives use narrow checks and a comment: + +.. code-block:: c++ + + *indices++ = value; // NOLINT(clang-analyzer-security.ArrayBound): loop + // iterates exactly N times; the analyzer cannot prove this from the macro. + + + .. _keyword-argument-processing: Keyword argument processing diff --git a/src/_macosx.m b/src/_macosx.m index 9ca6c0749322..62daf43abe1b 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -47,16 +47,20 @@ static void stopWithEvent() { [NSApp stop: nil]; // Post an event to trigger the actual stopping. - [NSApp postEvent: [NSEvent otherEventWithType: NSEventTypeApplicationDefined - location: NSZeroPoint - modifierFlags: 0 - timestamp: 0 - windowNumber: 0 - context: nil - subtype: 0 - data1: 0 - data2: 0] - atStart: YES]; + // +[NSEvent otherEventWithType:...] is declared nullable but will not return + // nil for these constant, valid arguments; guard defensively anyway. + NSEvent* event = [NSEvent otherEventWithType: NSEventTypeApplicationDefined + location: NSZeroPoint + modifierFlags: 0 + timestamp: 0 + windowNumber: 0 + context: nil + subtype: 0 + data1: 0 + data2: 0]; + if (event) { + [NSApp postEvent: event atStart: YES]; + } } // Signal handler for SIGINT, only argument matching for stopWithEvent @@ -375,11 +379,12 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) self->view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; int opts = (NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveInKeyWindow | NSTrackingInVisibleRect); - [self->view addTrackingArea: [ - [NSTrackingArea alloc] initWithRect: rect - options: opts - owner: self->view - userInfo: nil]]; + NSTrackingArea* area = [[NSTrackingArea alloc] initWithRect: rect + options: opts + owner: self->view + userInfo: nil]; + [self->view addTrackingArea: area]; + [area release]; [self->view setCanvas: (PyObject*)self]; exit: @@ -514,6 +519,8 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) static PyObject* FigureCanvas_stop_event_loop(FigureCanvas* self) { + // +[NSEvent otherEventWithType:...] is declared nullable but will not return + // nil for these constant, valid arguments; guard defensively anyway. NSEvent* event = [NSEvent otherEventWithType: NSEventTypeApplicationDefined location: NSZeroPoint modifierFlags: 0 @@ -523,7 +530,9 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) subtype: STOP_EVENT_LOOP data1: 0 data2: 0]; - [NSApp postEvent: event atStart: true]; + if (event) { + [NSApp postEvent: event atStart: true]; + } Py_RETURN_NONE; } @@ -767,6 +776,9 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) if (!PyArg_ParseTuple(args, "s", &title)) { return NULL; } + // PyArg_ParseTuple "s" guarantees valid UTF-8, so stringWithUTF8String: will + // not return nil here; the nullable annotation is a false positive. + // NOLINTNEXTLINE(clang-analyzer-nullability.NullablePassedToNonnull) [self->window setTitle: [NSString stringWithUTF8String: title]]; Py_RETURN_NONE; } @@ -885,7 +897,8 @@ - (void)save_figure:(id)sender; @implementation NavigationToolbar2Handler - (NavigationToolbar2Handler*)initWithToolbar:(PyObject*)theToolbar { - [self init]; + self = [self init]; + if (!self) { return nil; } toolbar = theToolbar; return self; } @@ -1005,8 +1018,10 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } rect.origin.y = 0.5*(height - rect.size.height); for (int i = 0; i < 7; i++) { + // PyArg_ParseTuple "s" guarantees valid UTF-8; stringWithUTF8String: will not return nil. NSString* filename = [NSString stringWithUTF8String: images[i]]; NSString* tooltip = [NSString stringWithUTF8String: tooltips[i]]; + // NOLINTNEXTLINE(clang-analyzer-nullability.NullablePassedToNonnull) NSImage* image = [[NSImage alloc] initWithContentsOfFile: filename]; buttons[i] = [[NSButton alloc] initWithFrame: rect]; [image setSize: size]; @@ -1074,7 +1089,9 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } NSTextView* messagebox = self->messagebox; if (messagebox) { + // PyArg_ParseTuple "s" guarantees valid UTF-8; stringWithUTF8String: will not return nil. NSString* text = [NSString stringWithUTF8String: message]; + // NOLINTNEXTLINE(clang-analyzer-nullability.NullablePassedToNonnull) [messagebox setString: text]; // Adjust width and height with the window size and content @@ -1131,6 +1148,8 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } } NSSavePanel* panel = [NSSavePanel savePanel]; [panel setTitle: [NSString stringWithUTF8String: title]]; + // PyArg_ParseTuple "s" guarantees valid UTF-8; stringWithUTF8String: will not return nil. + // NOLINTNEXTLINE(clang-analyzer-nullability.NullablePassedToNonnull) [panel setDirectoryURL: [NSURL fileURLWithPath: [NSString stringWithUTF8String: directory] isDirectory: YES]]; [panel setNameFieldStringValue: [NSString stringWithUTF8String: default_filename]]; @@ -1386,6 +1405,8 @@ - (void)windowWillClose:(NSNotification*)notification - (BOOL)windowShouldClose:(NSNotification*)notification { NSWindow* window = [self window]; + // +[NSEvent otherEventWithType:...] is declared nullable but will not return + // nil for these constant, valid arguments; guard defensively anyway. NSEvent* event = [NSEvent otherEventWithType: NSEventTypeApplicationDefined location: NSZeroPoint modifierFlags: 0 @@ -1395,7 +1416,9 @@ - (BOOL)windowShouldClose:(NSNotification*)notification subtype: WINDOW_CLOSING data1: 0 data2: 0]; - [NSApp postEvent: event atStart: true]; + if (event) { + [NSApp postEvent: event atStart: true]; + } if ([window respondsToSelector: @selector(closeButtonPressed)]) { BOOL closed = [((Window*) window) closeButtonPressed]; /* If closed, the window has already been closed via the manager. */ @@ -1616,7 +1639,12 @@ - (const char*)convertKeyEvent:(NSEvent*)event } [returnkey appendString:specialchar]; } else { - [returnkey appendString:[event charactersIgnoringModifiers]]; + // charactersIgnoringModifiers is nullable; guard defensively in case + // an unexpected event type reaches this path. + NSString* chars = [event charactersIgnoringModifiers]; + if (chars) { + [returnkey appendString:chars]; + } } } else { if (([event modifierFlags] & NSEventModifierFlagShift) || keyChangeShift) { diff --git a/tools/run_clang_tidy.py b/tools/run_clang_tidy.py new file mode 100644 index 000000000000..a61993f2479c --- /dev/null +++ b/tools/run_clang_tidy.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +""" +Run clang-tidy on Matplotlib's C/C++ and Objective-C sources. + +Build directory +--------------- +The script uses ``build/clang-tidy/`` as a dedicated meson build directory. +If it does not exist it is created automatically via ``meson setup`` +(no compilation takes place). + +Prerequisites +------------- +``meson``, ``pybind11``, and ``clang-tidy`` must be available. + +Usage +----- +:: + + python tools/run_clang_tidy.py +""" + +import re +import subprocess +import sys +from pathlib import Path + +_VENDORED_RE = re.compile(r"[\\/](extern|subprojects)[\\/]") +_DIAG_RE = re.compile(r"\S.*: (?:warning|error):") + + +def _filter_clang_tidy_output(text: str) -> str: + """ + Filter clang-tidy output to drop entire diagnostic chains that originate + in vendored code (``extern/`` or ``subprojects/``). + + clang-tidy output is structured as diagnostic groups: a ``warning:`` or + ``error:`` line followed by ``note:`` lines and source snippets, until the + next ``warning:``/``error:`` or end of output. If the triggering + diagnostic is in vendored code, the entire group (including notes that + reference our own ``src/`` files as execution-path context) is dropped. + """ + groups: list[list[str]] = [] + current: list[str] = [] + + for line in text.splitlines(): + if _DIAG_RE.match(line): + if current: + groups.append(current) + current = [line] + else: + current.append(line) + + if current: + groups.append(current) + + kept: list[str] = [] + for group in groups: + header = group[0] + if _DIAG_RE.match(header) and _VENDORED_RE.search(header): + continue + kept.extend(group) + + return "\n".join(kept) + + +def main() -> None: + repo_root = Path(__file__).resolve().parent.parent + build_dir = repo_root / "build" / "clang-tidy" + + result = subprocess.run( + ["meson", "setup", "--reconfigure", str(build_dir)], + cwd=repo_root, + ) + if result.returncode != 0: + sys.exit(result.returncode) + + result = subprocess.run( + [ + "meson", + "--internal", + "clangtidy", + str(repo_root), + str(build_dir), + "--color", + "never", + ], + cwd=build_dir, + capture_output=True, + text=True, + ) + + filtered = _filter_clang_tidy_output(result.stdout) + if filtered: + print(filtered) + if result.stderr: + print(result.stderr, end="", file=sys.stderr) + + sys.exit(result.returncode) + + +if __name__ == "__main__": + main() From 5449bea4e62358725b431641ff5d86cd95e5e5d4 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 11 May 2026 17:07:39 -0400 Subject: [PATCH 054/215] CI: add cool-down periods on dependabot --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d391ab4a18eb..3943b3719321 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,6 +5,8 @@ updates: directory: "/" schedule: interval: "weekly" + cooldown: + default-days: 7 groups: actions: patterns: @@ -13,9 +15,13 @@ updates: directory: "/" schedule: interval: "weekly" + cooldown: + default-days: 7 exclude-paths: - "ci/minver-requirements.txt" - package-ecosystem: "pre-commit" directory: "/" schedule: interval: "monthly" + cooldown: + default-days: 7 From edef1d8c6d620ff9f7f14493a0a6589d9dddfa92 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 11 May 2026 17:10:25 -0400 Subject: [PATCH 055/215] CI: remove redundancy and leaked credentials in workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aided by 🤖 --- .github/workflows/autoclose_schedule.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/autoclose_schedule.yml b/.github/workflows/autoclose_schedule.yml index 7e0fbd2055e9..a4eba770699c 100644 --- a/.github/workflows/autoclose_schedule.yml +++ b/.github/workflows/autoclose_schedule.yml @@ -23,15 +23,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: python-version: '3.13' - name: Install PyGithub run: pip install -Uq PyGithub - - name: Checkout repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Close PRs labeled more than 14 days ago run: | python tools/autoclose_prs.py From 628a5f5b69bdc7e82cbefbafc4825a4600af5df8 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 11 May 2026 17:06:30 -0400 Subject: [PATCH 056/215] BLD: add zizmor to prek config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added exceptions for things it thinks are questionable but we are OK with doing. Aided by 🤖 --- .github/zizmor.yml | 19 +++++++++++++++++++ .pre-commit-config.yaml | 4 ++++ 2 files changed, 23 insertions(+) create mode 100644 .github/zizmor.yml diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 000000000000..58b2d842b083 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,19 @@ +--- +rules: + dangerous-triggers: + ignore: + # These workflows use pull_request_target solely to obtain write access + # for API operations (labeling, commenting) on fork PRs. None of them + # check out or execute any PR-supplied code. + - autoclose_comment.yml:11 + - conflictcheck.yml:3 + - labeler.yml:3 + - pr_welcome.yml:4 + cache-poisoning: + ignore: + # cygwin.yml is a test-only workflow; no artifacts are published. + # The three caches (pip, ccache, matplotlib data) are purely for build + # acceleration and present no poisoning risk for released artifacts. + - cygwin.yml:144:9 + - cygwin.yml:151:9 + - cygwin.yml:158:9 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a70c32cc3aa..cc31f6adaac4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -82,6 +82,10 @@ repos: rev: 745eface02aef23e168a8afb6b5737818efbea95 # frozen: v0.11.0.1 hooks: - id: shellcheck + - repo: https://github.com/zizmorcore/zizmor-pre-commit + rev: a4727cbbcd26d7098e96b9cb738169b59711ae51 # frozen: v1.24.1 + hooks: + - id: zizmor - repo: https://github.com/python-jsonschema/check-jsonschema rev: 13614ab716a3113145f1294ed259d9fbe5678ff3 # frozen: 0.37.1 hooks: From 75b31925bf7c16c69694eed34991c16432689f6e Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 11 May 2026 17:08:10 -0400 Subject: [PATCH 057/215] CI: add zizmor action to keep us in line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aided by 🤖 --- .github/workflows/zizmor.yml | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/zizmor.yml diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 000000000000..60e99d4ea3bf --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,34 @@ +--- +name: zizmor + +on: + push: + branches: [main, v*.x] + pull_request: + branches: [main] + schedule: + - cron: '45 19 * * 1' + +permissions: {} + +jobs: + zizmor: + name: zizmor + if: github.repository == 'matplotlib/matplotlib' + runs-on: ubuntu-latest + permissions: + security-events: write + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3 + with: + advanced-security: true + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 050c2e83090ef6574dd42f6a65226288fd95d347 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 11 May 2026 23:08:04 -0400 Subject: [PATCH 058/215] CI: add static validation of svg MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aided by 🤖 --- .pre-commit-config.yaml | 8 ++++++ .svglintrc.mjs | 56 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 .svglintrc.mjs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cc31f6adaac4..354b030b8099 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -86,6 +86,14 @@ repos: rev: a4727cbbcd26d7098e96b9cb738169b59711ae51 # frozen: v1.24.1 hooks: - id: zizmor + - repo: https://github.com/simple-icons/svglint + rev: 8402586b94f073686e46707a163082e270ee5768 # frozen: v4.2.1 + hooks: + - id: svglint + # Override the top-level exclude so that mpl-data/images/ toolbar + # icons are also linted. Exemptions for the intentional interactive + # SVG examples are handled in .svglintrc.mjs. + exclude: '^$' - repo: https://github.com/python-jsonschema/check-jsonschema rev: 13614ab716a3113145f1294ed259d9fbe5678ff3 # frozen: 0.37.1 hooks: diff --git a/.svglintrc.mjs b/.svglintrc.mjs new file mode 100644 index 000000000000..7c6c3e194a64 --- /dev/null +++ b/.svglintrc.mjs @@ -0,0 +1,56 @@ +/** @type {import('svglint').Config} */ +const config = { + rules: { + // Ensure all SVGs are valid XML. + valid: true, + + // Block elements that can execute code or embed arbitrary content. + //