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/291] 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/291] 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/291] 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 f92a7e6eb7a2d685fd1b10d7219924a61f0f425e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 24 Apr 2026 01:06:19 -0400 Subject: [PATCH 004/291] DOC: Prepare GitHub stats for 3.11 --- doc/release/github_stats.rst | 1303 ++++++++++++++++- .../prev_whats_new/github_stats_3.10.9.rst | 103 ++ tools/github_stats.py | 2 + 3 files changed, 1353 insertions(+), 55 deletions(-) create mode 100644 doc/release/prev_whats_new/github_stats_3.10.9.rst diff --git a/doc/release/github_stats.rst b/doc/release/github_stats.rst index e01a1727b162..62c3242b7eb7 100644 --- a/doc/release/github_stats.rst +++ b/doc/release/github_stats.rst @@ -2,107 +2,1300 @@ .. _github-stats: -GitHub statistics for 3.10.9 (Apr 23, 2026) +GitHub statistics for 3.11.0 (Apr 24, 2026) =========================================== -GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2026/04/23 +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2026/04/24 These lists are automatically generated, and may be incomplete or contain duplicates. -We closed 10 issues and merged 34 pull requests. -The full list can be seen `on GitHub `__ +We closed 246 issues and merged 764 pull requests. +The full list can be seen `on GitHub `__ -The following 37 authors contributed 519 commits. +The following 264 authors contributed 4590 commits. +* 34j +* Aaratrika-Shelly +* Aaron Meurer * Aasma Gupta +* Abhiroop Batabyal +* Abitamim Bharmal +* Adam Ormondroyd +* AdamOrmondroyd +* Aditya Singh +* aditya-singh597 +* AdrashDec +* Aishling Cooke +* Alan Burlot +* Albert Y. Shih +* ALBIN BABU VARGHESE +* albus-droid +* Alexandra Khoo +* Allison +* alphanoobie +* Aman Kushwaha +* AMAN KUSHWAHA +* Aman Nijjar +* Aman Parganiha * Aman Srivastava +* Amisha Mehta +* amishamehta99 +* Amitesh Singh +* Anabelle VanDenburgh +* Andrea Alberti +* Andres Gutierrrez +* Andrew Landau +* Andrés Gutierrez +* Anselm Hahn +* anTon +* Anton * Antony Lee +* Archil Jain +* Arnaud Patard +* Barbier--Darnal Joseph * beelauuu +* Ben Greiner * Ben Root +* Bodhi Silberling +* Brian Christian +* BriAnna Foreman +* brk +* Carlos Ramos Carreño +* Cemonix +* Chaoyi Hu +* Charlie Thornton +* Chirag Sharma +* Chirag3841 +* chrisjbillington * Christine P. Chai +* clairefio +* Clemens Brunner +* Clément Robert +* cmp0xff +* Colton Lathrop +* Constantinos Menelaou +* Corenthin ZOZOR +* cvanelteren +* Daniel Weiss +* Danny +* David Lowry-Duda * David Stansby * dependabot[bot] +* DerWeh +* Diksha +* Dominik Stiller +* Doron Behar +* Duncan Macleod +* DWesl +* Edge-Seven +* ee25b003 +* ellie * Elliott Sales de Andrade -* G.D. McBain +* Emmanuel Ferdman +* EncryptedDoom +* Eric Firing +* Eric Larson +* Evgenii Radchenko +* Eytan Adler +* Fazeel Usmani +* founta +* francisayyad03 +* Francisco Cardozo +* G Karthik Koundinya +* G\. D\. McBain +* G26Karthik +* ganglike +* Geoffrey Thomas +* Gguidini * Greg Lucas +* guillermodotn * hannah +* Hannan7812 +* Hasan Rashid +* Hassan Kibirige +* heinrich5991 * hu-xiaonan +* Husain Gadiwala +* Ian Hunt-Isaak * Ian Thomas +* ianlv +* IdiotCoffee +* ilakk manoharan +* Ilakkuvaselvi Manoharan +* intelliking * Inês Cachola +* ishan372or +* James Addison +* Javier Pérez Robles +* jaya prajapati +* jayaprajapatii +* Jaylon +* Jimmy Shah +* jocelynvj +* JOD +* joddeepesh-cloud * Jody Klymak +* Johannes Kopton +* Jonas Drotleff +* Jonathan Reimer * Jouni K. Seppänen +* Julian Chen +* Kaustbh +* Kaustubh +* kdpenner * Khushi_29 +* Khushikela29 +* KIU Shueng Chuan +* konmenel +* Kris Rubiano +* kusch lionel +* Kyle Martin * Kyle Sunden +* Kyra Cho +* landoskape +* LangQi99 +* Larry Bradley +* leakyH +* Leo Singer +* Leon Merten Lohse +* lilfer +* litchi +* Logan Pageler +* Logan-Pageler +* Lucas Gruwez +* Lucx33 +* Luka Aladashvili +* Lukas Hergt +* lukashergt * Lumberbot (aka Jack) +* Lívia Lutz * m-sahare +* Mafalda Botelho +* Manit Roy +* manit2004 +* Manthan Nagvekar +* marbled-toast +* Marco Barbosa +* Marco Gorelli +* Marie +* Marten H. van Kerkwijk +* Marten Henric van Kerkwijk +* martincornejo +* masih.khatibzdeh +* Mateusz Sokół +* Matthew Feickert +* Melissa Weber Mendonça +* Melwyn Francis Carlo +* MengAiDev +* Milan Gittler +* MiniX16 +* Miriam +* Miriam Simone +* miriamsimone +* MKhatibzadeh +* Mohit Pal +* Moniza Kidwai +* MQY +* mromanie +* Muhammad Hannan Akram +* musvaage * N R Navaneet +* NabeelShar +* nakano * Nathan G. Wiseman +* Nathan Goldbaum +* Nathan Hansen +* Nathan McDougall +* Nick Coish +* Nicolai Weitkemper +* Niklas Mertsch +* null-dreams +* Obliman * Oscar Gustafsson +* Owl +* Parsa Homayouni +* Patrick Seitz +* Pedro Marques +* pedrom2002 +* Pieter Eendebak +* Pirzada Ahmad Faraz +* pirzada-ahmadfaraz * Praful Gulani +* Pranav +* Pranav Raghu +* pre-commit-ci[bot] +* proximalf +* q33566 * Qian Zhang +* r3kste +* Rafael Katri +* Rahul +* Rahul Monani * Raphael Erik Hviding * Raphael Quast +* RETHICK CB +* RogueRebel33 * Roman +* Roman A * Ruth Comer +* ruvilonix +* Ryan May +* Saakshi Gupta +* Sai Chaitanya, Sanivada * saikarna913 +* Sanchit Rishi +* Saumya * Scott Shambaugh +* Sebastien Wieckowski +* Siddharth_Savani +* Sonu Singh +* star1327p +* statxc +* Stefan van der Walt +* Stefan Vujadinovic * Steve Berardi +* Steve Nicholson +* tfpf * Thomas A Caswell +* thomashopkins32 +* Tiago Marques +* Tim Heap * Tim Hoffmann +* Timon Erhart +* Tine Zivic +* Tingwei Zhu * Trygve Magnus Ræder +* Ubuntu +* Vagner Messias +* Vedant Madane +* Victor Liu +* Vidya * Vikash Kumar +* Vishal Pankaj Chandratreya +* Vraj Rajpura +* Weh Andreas +* Wiliam +* Yuepeng Gu +* Zhongqi LUO +* ZPyrolink GitHub issues and pull requests: -Pull Requests (34): - -* :ghpull:`31556`: FIX: Inverted PyErr_Occurred check in enum type caster (_enums.h) -* :ghpull:`31078`: Backport PR #31075 on branch v3.10.x (Fix remove method for figure title and xy-labels) -* :ghpull:`31280`: Backport PR #31278 on branch v3.10.x (Fix ``clabel`` manual argument not accepting unit-typed coordinates) -* :ghpull:`31520`: Backport PR #31020 on branch v3.10.x (DOC: Fix doc builds with Sphinx 9) -* :ghpull:`31511`: Backport PR #31504 on branch v3.10.x (Re-order variants to prioritize narrower types) -* :ghpull:`31504`: Re-order variants to prioritize narrower types -* :ghpull:`31445`: Backport PR #31437: mathtext: Fix type inconsistency with fontmaps -* :ghpull:`31437`: mathtext: Fix type inconsistency with fontmaps -* :ghpull:`31411`: Backport PR #31323 on branch v3.10.x (FIX: Prevent crash when removing a subfigure containing subplots) -* :ghpull:`31421`: Backport PR #31420 on branch v3.10.x (Fix outdated Savannah URL for freetype download) -* :ghpull:`31420`: Fix outdated Savannah URL for freetype download -* :ghpull:`31418`: Backport PR #31401: BLD: Temporarily pin setuptools-scm<10 -* :ghpull:`31323`: FIX: Prevent crash when removing a subfigure containing subplots -* :ghpull:`31401`: BLD: Temporarily pin setuptools-scm<10 -* :ghpull:`31278`: Fix ``clabel`` manual argument not accepting unit-typed coordinates -* :ghpull:`31154`: Backport PR #31153 on branch v3.10.x (TST: Use correct method of clearing mock objects) -* :ghpull:`31153`: TST: Use correct method of clearing mock objects -* :ghpull:`31075`: Fix remove method for figure title and xy-labels -* :ghpull:`31036`: Backport PR #31035 on branch v3.10.x (DOCS: Fix typo in time array step size comment) -* :ghpull:`30986`: Backport PR #30985 on branch v3.10.x (MNT: do not assign a numpy array shape) -* :ghpull:`30985`: MNT: do not assign a numpy array shape -* :ghpull:`30971`: Backport PR #30969 on branch v3.10.x (DOC: Simplify barh() example) -* :ghpull:`30965`: Backport PR #30952 on branch v3.10.x (DOC: Tutorial on API shortcuts) -* :ghpull:`30964`: Backport PR #30960 on branch v3.10.x (SVG backend - handle font weight as integer) -* :ghpull:`30960`: SVG backend - handle font weight as integer -* :ghpull:`30924`: Backport PR #30910 on branch v3.10.x (DOC: Improve writer parameter docs of Animation.save()) -* :ghpull:`30870`: Backport PR #30869 on branch v3.10.x (FIX: Accept array for zdir) -* :ghpull:`30869`: FIX: Accept array for zdir -* :ghpull:`30860`: Backport PR #30858 on branch v3.10.x (DOC: reinstate "codex" search term) -* :ghpull:`30818`: Backport PR #30817 on branch v3.10.x (Update sphinx-gallery header patch) -* :ghpull:`30801`: Backport PR #30763 on branch v3.10.x (DOC: Add example how to align tick labels) -* :ghpull:`30791`: Backport PR #30788 on branch v3.10.8-doc (Fix typo in key-mapping for "f11") -* :ghpull:`30790`: Backport PR #30788 on branch v3.10.x (Fix typo in key-mapping for "f11") -* :ghpull:`30788`: Fix typo in key-mapping for "f11" - -Issues (10): - -* :ghissue:`31495`: Unavoidable warnings with pybind11 main branch -* :ghissue:`31433`: [MNT]: Mypy error -* :ghissue:`31340`: [Bug]: outdated savannah URL in subprojects/freetype-2.6.1.wrap -* :ghissue:`31319`: [Bug]: Crash when removing a subfigure with a subplot in a figure -* :ghissue:`27525`: [Bug]: clabel manual argument does not accept units -* :ghissue:`31112`: [TST] Upcoming dependency test failures -* :ghissue:`31073`: [Bug]: Crash when Removing Suptitle in a Figure with Constrained Layout -* :ghissue:`30981`: [TST] Upcoming dependency test failures -* :ghissue:`30868`: [Bug]: Axe3D text() method does not allow zdir=numpy.array(...) -* :ghissue:`21566`: [ENH]: set_horizontalalignment("right") on Y axis labels when yaxis.ticks_right() is used. +Pull Requests (764): + +* :ghpull:`31561`: Fixed bug with an uninitialized colormap in parallel threads +* :ghpull:`31555`: FIX: removing colorbar's axes also removes colorbar +* :ghpull:`31560`: merge up v3.10.9 +* :ghpull:`31416`: MNT: Privatize Formatter attributes +* :ghpull:`23616`: feat(mathtext): support underline +* :ghpull:`31554`: BUG: avoid a deprecation warning from numpy 2.5 (calling ``datetime64('NaT')`` without a unit is deprecated) +* :ghpull:`31535`: DOC: fix broken link to wxPython Widget Inspection Tool +* :ghpull:`31551`: Bump https://github.com/pre-commit/mirrors-mypy from v1.20.1 to 1.20.2 +* :ghpull:`31552`: Bump scientific-python/upload-nightly-action from 0.6.3 to 0.6.4 in the actions group +* :ghpull:`31478`: Fix errorbar autoscaling inconsistency on log axes +* :ghpull:`31522`: MNT: Update all pre-commit hooks +* :ghpull:`31365`: Add thumbnail for embedding in user interfaces examples +* :ghpull:`31530`: BUG: Fix relim() to support Collection artists (scatter, etc.) +* :ghpull:`31514`: Add suggestions to more lookup errors +* :ghpull:`31465`: lib/matplotlib/tests/test_inset.py: Fix tolerance on aarch64 +* :ghpull:`31521`: Drop support for font hinting factor +* :ghpull:`31492`: MNT: Ensure all types from matplotlib.typing are documented +* :ghpull:`31524`: FIX: Disallow twinx/twiny on Axes3D +* :ghpull:`31540`: DOC: replace dolphin license RDF block with prose attribution +* :ghpull:`31426`: Fix: Optimize Cursor clearing on mouse exit to prevent lag +* :ghpull:`31512`: Document that ``TimedAnimation`` should not be used +* :ghpull:`31518`: DOC: add tags to tick locator and formatter examples +* :ghpull:`31519`: Bump the actions group with 3 updates +* :ghpull:`31517`: [DOC] make headers in pie example consistent +* :ghpull:`31515`: Remove unnecessary ruff lint exceptions +* :ghpull:`31516`: TST: account for flakiness with Numpy v1 (part 3) +* :ghpull:`31489`: Fixed: specified exception type in cbook.py +* :ghpull:`31314`: DOC: setting active axes position is ineffective +* :ghpull:`31148`: TST: Use explicit style in all image_comparison calls +* :ghpull:`31486`: ENH: Add an environment variable to ignore system fonts +* :ghpull:`31507`: PR template: always ask for AI declaration +* :ghpull:`31503`: TST: Harden handling of Popen subprocesses +* :ghpull:`31490`: DOC: Minor style improvement of radio buttons examples +* :ghpull:`31181`: ENH: Give control whether twinx() or twiny() overlays the main axis +* :ghpull:`31485`: MNT: Update bundled font libraries +* :ghpull:`31484`: MNT: Use new defaults in set_font_settings_for_testing +* :ghpull:`31483`: Bump the actions group across 1 directory with 2 updates +* :ghpull:`31476`: DOC: Improve Radio Buttons example +* :ghpull:`31275`: DOC: use minigallery for tutorial thumbnails +* :ghpull:`29763`: Shorten Agg template usage with class template argument deduction. +* :ghpull:`31353`: Fix #21409: Make twin axes inherit parent position +* :ghpull:`31431`: FIX: Guard against already-removed labels in ContourSet.remove() +* :ghpull:`31428`: Relax type hints for xy and xytext in annotate +* :ghpull:`31468`: DOC: Replace ``skip_deprecated`` extension by standard Sphinx metadata +* :ghpull:`30161`: Font and text overhaul +* :ghpull:`31461`: Support font features/language in default RendererBase.draw_text +* :ghpull:`31303`: TST: Reset tolerances on tests changed by text overhaul +* :ghpull:`31471`: DOC: Use FuncAnimation in 3D animations +* :ghpull:`31477`: DOC: Improve Radio Buttons Grid example +* :ghpull:`31470`: MNT: Deprecate matplotlib.image.thumbnail +* :ghpull:`31475`: Purge gitter links +* :ghpull:`31466`: DOC: make simple animation example easier to find +* :ghpull:`31469`: Change if condition to allow handles to be passed as a ndarray and not only Python list or tuple, etc. +* :ghpull:`31459`: DOC: Improve AI policy +* :ghpull:`31444`: Bump the actions group with 3 updates +* :ghpull:`31456`: Clarify fonttype switch in backend_pdf. +* :ghpull:`31300`: TST: Set tests touched by text overhaul to mpl20 style +* :ghpull:`31449`: Fix: improve log-scale error message wording +* :ghpull:`30385`: Add type stubs for functions in matplotlib.dates +* :ghpull:`31442`: TST: account for flakiness with Numpy v1 (part 2) +* :ghpull:`31440`: Fix FreeType runtime version check +* :ghpull:`31295`: TST: Cleanup back-compat code in tests touched by text overhaul +* :ghpull:`31408`: Merge branch 'main' into text-overhaul +* :ghpull:`31407`: BLD: Update bundled FreeType to 2.14.3 +* :ghpull:`31439`: Clarify SecondaryAxes limit behavior via documentation +* :ghpull:`31432`: DOC: More concise page title: Development setup +* :ghpull:`31423`: DOC: Remove pyplot vs. OO interface discussion from lifecycle example +* :ghpull:`31413`: ENH: Support partial figsize with None (#31400) +* :ghpull:`31368`: Fix: Prevent Cursor blitting from erasing overlapping axes (#25670) +* :ghpull:`31409`: Bump the actions group with 2 updates +* :ghpull:`31417`: DOC: Explain return value of secondary_x/yaxis +* :ghpull:`31412`: MNT: Minor cleanup of label formatting in PathCollection.legend_elements +* :ghpull:`31422`: Improve legend loc and bbox_to_anchor documentation (#26620) +* :ghpull:`31414`: DOC: Improve Formatter documentation +* :ghpull:`31419`: Add a short example to StrMethodFormatter docstring +* :ghpull:`31405`: Tweak secondary_{x,y}axis docs. +* :ghpull:`31372`: BLD: Update bundled libraqm to 0.10.4 +* :ghpull:`31198`: Allow tuning the shape of {L,R,D}Arrow tips. +* :ghpull:`31183`: ENH: Allow fonts to be addressed by any of their SFNT family names +* :ghpull:`31371`: ps/pdf: Override font height metrics to support AFM files +* :ghpull:`31343`: TST: Restore some tolerances for some arch/platform-specific failures +* :ghpull:`31248`: SEC: Remove eval() from validate_cycler +* :ghpull:`31395`: doc: mention ``bar_label`` in ``bar`` and ``barh`` +* :ghpull:`31385`: Make font search case insensitive in logo example +* :ghpull:`31399`: DOC: Rename gallery README.txt files to GALLERY_HEADER.rst +* :ghpull:`29998`: Implement head resizing (and reversal) for larrow/rarrow/darrow +* :ghpull:`24744`: Addresses issue #24618 "Road sign" boxstyle/annotation, alternative to #24697 +* :ghpull:`31392`: Tweak Formatter method docstrings. +* :ghpull:`31200`: DOC: moderation and enforcement +* :ghpull:`30513`: TST: Remove redundant font tests +* :ghpull:`31363`: Update black requirement from <26 to <27 +* :ghpull:`31355`: Bump the actions group across 1 directory with 8 updates +* :ghpull:`31370`: Update dead link for Ware 1988 in colormap docs +* :ghpull:`31357`: ci: Configure dependabot to skip minver requirements +* :ghpull:`31358`: TST: Replace pywin32 with ctypes wrapper +* :ghpull:`29281`: Port requirements to PEP735 +* :ghpull:`31347`: FIX: Deprecate using clabel() with filled contours +* :ghpull:`31349`: DOC: Correct a few typos in documentation +* :ghpull:`31244`: PERF: Sticky edges speedup +* :ghpull:`31306`: [MNT]: Implement ``Scale.val_in_range`` and refactor ``_point_in_data_domain`` +* :ghpull:`31291`: text: Use font metrics to determine line heights +* :ghpull:`30900`: Added Turbo License doc +* :ghpull:`31307`: FIX: avoid applying dashed patterns to zero-width lines and patches +* :ghpull:`31338`: MAINT: Fix formatting on autoclose bot message +* :ghpull:`31313`: Fixed lingering bugs with image rendering related to exact half display pixels +* :ghpull:`31329`: DOC: Add note about opening multiple PRs +* :ghpull:`29093`: Add wasm CI +* :ghpull:`31283`: MNT: Add autoclose bot inspired by scikit-learn +* :ghpull:`31322`: DOC: fix pcolormesh doc +* :ghpull:`31308`: DOC: Add thumbnail for multipage_pdf gallery example +* :ghpull:`31315`: [BUG] Warn when legend() receives mismatched handles and labels in 2-argument positional form +* :ghpull:`31251`: Emit xlim_changed / ylim_changed when limits expand via set_xticks / set_yticks +* :ghpull:`31316`: DOC: clarify explanation of axline in infinite lines example +* :ghpull:`31309`: DOC: update pandas intersphinx mapping +* :ghpull:`31281`: Drop axis_artist tickdir image compat, due to text-overhaul merge. +* :ghpull:`31294`: MNT: Restrict webagg toolbar actions to valid actions +* :ghpull:`31282`: SEC: Block shell escapes in latex and ps commands +* :ghpull:`31252`: DOC: Fix rendering of quiver documentation +* :ghpull:`31285`: ENH: Ignore empty text for tightbbox +* :ghpull:`31230`: API: Raise ValueError in subplots if num refers to existing figure +* :ghpull:`31133`: fix: resolve FigureCanvasTkAgg clipping on Windows HiDPI +* :ghpull:`30908`: mathtext support for \phantom, \llap, \rlap for faking text metrics. +* :ghpull:`31261`: Bump the actions group with 2 updates +* :ghpull:`30369`: Support standard tickdir control (in/out/inout) in axisartist. +* :ghpull:`27987`: qhull: Fix inconsistent formatting function arguments +* :ghpull:`31061`: BUG: Fix text appearing far outside valid axis scale range +* :ghpull:`31117`: Clarify introductory description in scatter_star_poly example. +* :ghpull:`31203`: Fix Axes.hist crash for numpy timedelta64 inputs +* :ghpull:`31262`: DOC: Correct ``byweekday`` description in ``WeekdayLocator`` +* :ghpull:`31260`: MNT: Raise NotImplementedError for 3D semilog plots +* :ghpull:`31143`: Deprecate public access to XMLWriter; simplify some attribute settings +* :ghpull:`31258`: DOC: Document that set_aspect applies the aspect lazily +* :ghpull:`31005`: PERF: Bezier root finding speedup +* :ghpull:`30980`: Fix 3D axes to properly support non-linear scales (log, symlog, etc.) +* :ghpull:`30844`: allow passing a function to ``CallbackRegistry.disconnect_func`` +* :ghpull:`30995`: PERF: Speed up ticks processing when not visible or using a NullLocator +* :ghpull:`31128`: Fix relim() ignoring scatter PathCollection offsets +* :ghpull:`31166`: Add private Artist-level autoscale participation flag +* :ghpull:`31238`: CI: Explicitly define CI workflow permissions +* :ghpull:`31228`: Bump the actions group with 3 updates +* :ghpull:`29469`: MNT: Separate property cycle handling from _process_plot_var_args +* :ghpull:`31121`: mathtext: add mathnormal and distinguish between normal and italic family +* :ghpull:`31170`: Cleanup QuiverKey init and deprecate some attributes. +* :ghpull:`31004`: PERF: More speedups +* :ghpull:`31226`: ft2font: Read more entries from OS/2 font table +* :ghpull:`31191`: TST: Switch mathtext tests to mpl20 +* :ghpull:`31231`: DOC: make nightly download command one line so it works on Windows +* :ghpull:`30754`: MNT: Improve Grouper +* :ghpull:`31236`: DOC: Remove gitter links and direct folks to Discourse chat +* :ghpull:`31145`: ENH: Snap 3D view angle changes when holding Control key +* :ghpull:`31179`: Remove mpl.text._get_textbox. +* :ghpull:`31202`: ENH: Adds ``errorbar.capthick`` and ``errorbar.elinewidth`` to mplstyle +* :ghpull:`31222`: DOC: Rewrite tickabel rotation example to use rotation_mode +* :ghpull:`31001`: PERF: Text handling speedups +* :ghpull:`30975`: Use LOCALAPPDATA for config/cache directories on Windows +* :ghpull:`30795`: Fix array alpha to multiply (not replace) existing RGBA alpha +* :ghpull:`31021`: Fixed inaccurate image placement and even more resampling bugs +* :ghpull:`31110`: mathtext: Fetch quad width & axis height from font metrics +* :ghpull:`31193`: DOC: Clarify computed_zorder applies to Collections and Patches only +* :ghpull:`31217`: DOC: use pivot='middle' instead of 'mid' in quiver demo +* :ghpull:`31212`: DOC: discourage pivot='mid' for quiver +* :ghpull:`31204`: Reword the "fully-new contributor" section. +* :ghpull:`31201`: DOC: Add sections to rcParams documentation +* :ghpull:`31196`: DOC: Document which files need to be updated for new rcparams +* :ghpull:`31163`: DOC: update new contributor guidance re timelines, AI, reaching out +* :ghpull:`31124`: MAINT: add AI disclosure to pr template +* :ghpull:`31076`: Avoid using pyplot for check_figures_equal +* :ghpull:`31189`: Bump the actions group with 2 updates +* :ghpull:`31188`: Remove use of the discouraged plt.imread() in the docs. +* :ghpull:`31007`: TST: Skip tests that use a large amount of memory by default +* :ghpull:`30967`: ENH: Implement gapcolor for patch edges +* :ghpull:`31142`: doc: explain that gfi is for training and add no AI policy +* :ghpull:`31137`: TST: Simplify image testing decorator calls +* :ghpull:`31119`: MNT: Normalize internal set_foreground calls to RGBA +* :ghpull:`31107`: Fix confusion between text height and ascent in metrics calculations. +* :ghpull:`31168`: Fix docstring ``lib/matplotlib/pyplot.py`` and related ``lib/matplotlib/__init__.py`` +* :ghpull:`31167`: Copy-edit the transform tutorial. +* :ghpull:`31160`: Bump the actions group across 1 directory with 4 updates +* :ghpull:`29374`: DOC: Emphasize artist as annotation in AnnotationBbox demo and add to annotation guide +* :ghpull:`31151`: Add mlx support +* :ghpull:`31141`: Fix mutable default arguments in backend_svg.py +* :ghpull:`31140`: DOC: Document set_figure() is a low-level API +* :ghpull:`31026`: DOC: Explicitly prohibit bots/agents to post contents +* :ghpull:`31131`: MAINT: added don't solve AI note to gfi bot +* :ghpull:`31043`: MAINT: new contributor bot ask for AI usage +* :ghpull:`30803`: {Radio,Check}Buttons: Add 2D grid labels layout support +* :ghpull:`31111`: Remove some code for compatibility with pyparsing<3 +* :ghpull:`31046`: Implement TeX's fraction and script alignment +* :ghpull:`31085`: Refactor RendererAgg.draw_{mathtext,text,tex} to use same base algorithm +* :ghpull:`28814`: patheffects.SimpleLineShadow calling non-existent get_foreground method from GraphicsContextBase +* :ghpull:`31090`: MAINT: Move to first-contribution action +* :ghpull:`31069`: Fix positioning of wide mathtext accents. +* :ghpull:`30938`: Update bundled FreeType and HarfBuzz libraries +* :ghpull:`31091`: BUG: Fix IndexLocator.tick_values returning values greater than vmax +* :ghpull:`31050`: ft2font: Extend OS/2 table with new fields +* :ghpull:`30039`: Rasterize dvi files without dvipng. +* :ghpull:`31081`: Switch from pre-commit to prek +* :ghpull:`30993`: PERF: Speed up log and symlog scale transforms +* :ghpull:`31082`: MNT: Rename check_getitem to getitem_checked +* :ghpull:`31080`: DOC: Fix missing references for updated FT2Font.set_text +* :ghpull:`30746`: Fix PDF bloat for off-axis scatter with per-point colors +* :ghpull:`31062`: Bump the actions group across 1 directory with 4 updates +* :ghpull:`31063`: Merge main back into text-overhaul branch +* :ghpull:`31056`: Keep mathtext boxes in xywh representation throughout. +* :ghpull:`31060`: MNT: Remove unused eventson context from artist property update +* :ghpull:`31059`: PERF: Refactor bezier poly coefficient calcs for speedup +* :ghpull:`31000`: PERF: Skip kwargs normalization in Artist._cm_set +* :ghpull:`31028`: DOC: Generate rcParams docs directly during build +* :ghpull:`31058`: TST: add basic test for set +* :ghpull:`31057`: DOC: Clarify Artist.set() behavior +* :ghpull:`31041`: Add tests for invalid properties and duplicate aliases in Artist.set +* :ghpull:`30978`: MNT: Discourage Artist.update +* :ghpull:`31016`: Doc: Clarify default levels behavior in contour/contourf +* :ghpull:`31031`: RadioButtons: fix self._clicked method (followup to #30997) +* :ghpull:`30059`: Drop the FT2Font intermediate buffer. +* :ghpull:`31013`: docs: improve contour docstring and wrap long lines +* :ghpull:`31044`: fix for sphinx_gallery < 0.16.0 +* :ghpull:`31033`: Add type hint for fig_kw in subplots +* :ghpull:`31030`: DOC: bring the credits page a little more up-to-date +* :ghpull:`31034`: DOC: Make grammatical corrections to documentation +* :ghpull:`30752`: Improving error message for width and position type mismatch in violinplot +* :ghpull:`31023`: Speedup normalize_kwargs by storing aliases in a more practical format. +* :ghpull:`31014`: TST: Fix warnings from Pillow for unavailable features +* :ghpull:`30935`: FIX: Handle AxesWidget cleanup after failed init +* :ghpull:`31020`: DOC: Fix doc builds with Sphinx 9 +* :ghpull:`31025`: DOC: move doc build options into tables and tabs +* :ghpull:`31024`: Fix formatting: add space after # in TODO comment +* :ghpull:`30997`: widgets: use a shared _Buttons class for {Radio,Check}Buttons +* :ghpull:`31010`: DOC: update and slightly reorg docs docs +* :ghpull:`31011`: Fix grammar: 'it would better' -> 'it would be better' in comment +* :ghpull:`31002`: Remove outdated notion of property alias priority from docs. +* :ghpull:`29881`: feat(CI): add Codecov Test Analytics for flaky and failed tests +* :ghpull:`30999`: Bump the actions group across 1 directory with 2 updates +* :ghpull:`30991`: Improve findfont cache invalidation. +* :ghpull:`30992`: Fix typo: remove extra space in MultiCursor deprecation message +* :ghpull:`30984`: DOC: update interactive rebase instructions +* :ghpull:`27946`: Add support for horizontal CheckButtons +* :ghpull:`30778`: MNT: remove decorator frames from traceback +* :ghpull:`30838`: Do not fail when markers are numpy integers +* :ghpull:`30977`: Revert exception handling case after numpy minver bump to 1.25 +* :ghpull:`30849`: Fix Axes.grid() to respect alpha in color tuples +* :ghpull:`30939`: DOC: Improve widgets API documentation +* :ghpull:`30970`: DOC: Move spectral plot examples from lines to statistics +* :ghpull:`30945`: Prevent blitting errors after canvas swap in RadioButtons and CheckButtons +* :ghpull:`30184`: Fixed several accuracy bugs with image resampling +* :ghpull:`30973`: DOC: modernise barh example +* :ghpull:`30956`: DOC: Some small additions to the API docs +* :ghpull:`30959`: DOC: Clarify matplotlib vs. matplotlib-base in conda +* :ghpull:`30950`: TST: account for flakiness with Numpy v1 +* :ghpull:`30954`: Fix trivial typo in example. +* :ghpull:`30947`: TST: always force the SETUPTOOLS_SCM version in test subprocesses +* :ghpull:`30949`: Add uv.lock to .gitignore +* :ghpull:`30948`: DOC: Improve linkage between rcParams-related documentation +* :ghpull:`30871`: Define the supported rcParams as code +* :ghpull:`30886`: BUG: Fix Windows subprocess timeouts with CREATE_NO_WINDOW flag +* :ghpull:`30777`: DOC: Introduce backend versions +* :ghpull:`30824`: Fixed bilinear interpolation for ``SegmentedBivarColormap`` +* :ghpull:`30942`: Bump pypa/cibuildwheel from 3.3.0 to 3.3.1 in the actions group +* :ghpull:`30918`: TST: account for asyncio changes in py314 +* :ghpull:`30937`: Merge branch 'v3.10.x' into main +* :ghpull:`30936`: DOC: Clarify data inputs for boxplot() and violinplot() +* :ghpull:`30855`: DOC: Clarify and unify set_linestyle +* :ghpull:`30921`: Exclude confirmed bugs from stale bot +* :ghpull:`30892`: Bump the actions group across 1 directory with 11 updates +* :ghpull:`30920`: FIX: Increase reruns for flaky test_invisible_Line_rendering (#30809) +* :ghpull:`30889`: MNT: Make transforms helper functions private +* :ghpull:`30922`: Reduce stale bot to run once per week +* :ghpull:`30912`: Pcolormesh Doc Fix +* :ghpull:`30916`: Docs: Remove outdated annotate_transform example, link to annotation tutorial +* :ghpull:`30919`: DOC: Correct typos on a/an usage including print messages +* :ghpull:`30914`: Fix outdated documentation links for violin/boxplot example +* :ghpull:`30907`: Inline intermediate constructs in axisartist demos. +* :ghpull:`30867`: Handle single color for multiple datasets in ``hist`` +* :ghpull:`30591`: FIX: Make widget blitting compatible with swapped canvas +* :ghpull:`30821`: Implements the Okabe-Ito accessible colormap. +* :ghpull:`30737`: Deprecate unused canvas parameter to MultiCursor +* :ghpull:`29966`: Fix AxesWidgets on inset_axes that are outside their parent. +* :ghpull:`30600`: Implement warning for Text3D's rotation/rotation_mode parameters +* :ghpull:`30847`: Fix test_ensure_multivariate_data on 32-bit systems +* :ghpull:`30856`: DOC: Rectangle: Link to FancyBboxPatch for rounded corners +* :ghpull:`30854`: DOC: Improve docs of legend loc=best +* :ghpull:`30863`: Fix macOS toolbar crash +* :ghpull:`30853`: Minor doc fixes re: close()ing figures. +* :ghpull:`30846`: Add pixi and uv install options to bug template +* :ghpull:`30842`: Update release docs for new publish workflow, remove old publish step +* :ghpull:`30841`: Add type annotation for LocationEvent.modifiers +* :ghpull:`30775`: FIX: figureoptions updates title string only +* :ghpull:`30726`: Enh/Add hatch pattern support to Axes.grouped_bar +* :ghpull:`30808`: Consolidate style parameter handling for plotting methods that call other plotting methods +* :ghpull:`30815`: MNT: Fix handling of ints in rgb_to_hsv() +* :ghpull:`30533`: gtk: Add more explicit version requirements +* :ghpull:`30835`: Improve error messages for mismatched s arg to scatter(). +* :ghpull:`30750`: FIX: when creating a canvas from a Figure use original dpi +* :ghpull:`30822`: DOC: Define the effect of rcParams["figure.raise_window"] = False +* :ghpull:`30052`: Setting imshow(animated=True) still show does not show an image +* :ghpull:`30820`: DOC: Add parameters documentation for FFMpegFileWriter +* :ghpull:`30816`: Fix typos in API interfaces documentation +* :ghpull:`30814`: DOC: Discouraged duplicate colormaps +* :ghpull:`30813`: Add legend.linewidth to rcParam type hint +* :ghpull:`30705`: Add testing for rcParams Literal type hints +* :ghpull:`30812`: DOC: remove duplicate whatsnew heading +* :ghpull:`30810`: Fix rstcheck failures +* :ghpull:`30334`: Add support for loading all fonts from collections +* :ghpull:`30760`: Fix axis3d to include offset text in tight bounding box calculation +* :ghpull:`30780`: Add legend.linewidth parameter to control legend box edge linewidth +* :ghpull:`30799`: DOC: don't index or unpack the return value of pie +* :ghpull:`30766`: Fix colorbar alignment with suptitle in compressed layout mode +* :ghpull:`30756`: Add legend support for PatchCollection +* :ghpull:`30782`: DOC: Reintroduce glossary +* :ghpull:`29494`: github: added explicit do not merge label to label check +* :ghpull:`30784`: correct statement about available methods in ``Quiver`` docstring +* :ghpull:`30733`: ENH: introduce PieContainer and pie_label method +* :ghpull:`30783`: DOC: Add example usage to make_keyword_only() +* :ghpull:`30776`: MNT: Declare table() to be not further developed +* :ghpull:`30774`: DOC: Fix documentation error of hexbin +* :ghpull:`30607`: Implement libraqm for vector outputs +* :ghpull:`30753`: Update mpl-sphinx-theme in environment.yml +* :ghpull:`30699`: [DOC] dev landing page admonition about AI usage/link to policy +* :ghpull:`30761`: DOC: Clarify restrictions on GenAI usage +* :ghpull:`30724`: Bump github/codeql-action from 4.31.0 to 4.31.2 in the actions group +* :ghpull:`30665`: Grammar corrections in User guide FAQ +* :ghpull:`30741`: Add :code-caption: option to plot directive +* :ghpull:`30736`: DOC: Correct grammatical issues especially on a/an usage +* :ghpull:`30627`: Remove forced fallback from FT2Font::load_char +* :ghpull:`30715`: Fix spacing in r"$\max f$". +* :ghpull:`30723`: Add file extension to whatsnew entry +* :ghpull:`30690`: Bump the actions group with 3 updates +* :ghpull:`30560`: Consistent zoom boxes +* :ghpull:`30565`: fix: Qt5Agg support darkmode icon by using svg +* :ghpull:`29989`: fix: Fix unstable tkagg small plot size. +* :ghpull:`30708`: doc: make external scipy link explicit +* :ghpull:`30511`: Update Colorizer/ColorizingArtist to work with MultiNorm +* :ghpull:`30696`: FIX: Account for horizontal/vertical lines in tightbox +* :ghpull:`30316`: Create RCKeyType +* :ghpull:`30686`: DOC: Remove notebook instructions from image tutorial +* :ghpull:`30684`: Update README links to static images +* :ghpull:`30640`: Bump the actions group across 1 directory with 6 updates +* :ghpull:`30677`: Merge branch 'main' into text-overhaul +* :ghpull:`30668`: cibw: Switch macos 13 to 15 Intel +* :ghpull:`30667`: DOC: Correct typos: lets -> let's [ci docs] +* :ghpull:`28831`: Improve the cache when getting font metrics +* :ghpull:`30655`: simplify ContourSet.draw +* :ghpull:`30652`: Stale action: sort issues by last updated +* :ghpull:`30636`: FIX: Keep legacy alpha behavior for violinplot without facecolor +* :ghpull:`30646`: merge up v3.10.7 +* :ghpull:`30639`: DOC: Add note about linear colorbar scale option for TwoSlopeNorm +* :ghpull:`30629`: Fix test_mult_norm_call_types on 32-bit systems +* :ghpull:`30634`: Don't force axes limits in hist2d. +* :ghpull:`29221`: Multivariate plotting in imshow, pcolor and pcolormesh +* :ghpull:`30630`: Update first-interaction from v3.0.0 to v3.1.0 +* :ghpull:`29695`: Add font feature API to Text +* :ghpull:`30608`: Prepare ``CharacterTracker`` for advanced font features +* :ghpull:`30531`: MNT: Pending-deprecate setting colormap extremes in-place +* :ghpull:`30543`: ENH: support x/y-axis zoom +* :ghpull:`30590`: MNT: Define Protocol for Animation.event_source +* :ghpull:`30619`: Include step info in str(scroll_event). +* :ghpull:`30620`: Add --debug flag to python -mmatplotlib.dviread CLI. +* :ghpull:`30499`: Improve cursor icons with RectangleSelector +* :ghpull:`30610`: Bump mpl-sphinx-theme version +* :ghpull:`30615`: Use auto to remove long typedefs in dlsym/GetProcAddress calls. +* :ghpull:`30616`: DOC: add what's new info for violin_stats +* :ghpull:`30606`: DOC: Fix raw string in mathtext unicode example +* :ghpull:`30603`: MNT: Fix some broken deprecations +* :ghpull:`30512`: pdf: Improve text with characters outside embedded font limits +* :ghpull:`29936`: Fix auto-sized glyphs with BaKoMa fonts +* :ghpull:`30573`: Add os.PathLike support to FT2Font constructor, and FontManager +* :ghpull:`30595`: ft2font: Split layouting from set_text +* :ghpull:`30596`: Cleanup donuts example. +* :ghpull:`29794`: Add language parameter to Text objects +* :ghpull:`30583`: MNT: Streamline deferred initialization of Colormap +* :ghpull:`30582`: MNT: Do not use colormap setters in tests +* :ghpull:`30567`: pdf: Merge loops for single byte text chunk output +* :ghpull:`30579`: Merge main back into text-overhaul branch to fix CI +* :ghpull:`30586`: ci: Bump Ubuntu ARM builder to 24.04 +* :ghpull:`30581`: TST: Force Agg backend in test_openin_any_paranoid +* :ghpull:`30569`: Copy-edit the "fonts in pdf and postscript" table. +* :ghpull:`30208`: Make path extension a bit safer +* :ghpull:`30577`: MNT: Move all Colormap extremes setter logic into a single _set_extremes() +* :ghpull:`30562`: DOC: improve description of boilerplate.py +* :ghpull:`30566`: pdf/ps: Track full character map in CharacterTracker +* :ghpull:`30335`: Use glyph indices for font tracking in vector formats +* :ghpull:`30561`: Bump github/codeql-action from 3.30.1 to 3.30.3 in the actions group +* :ghpull:`29855`: ENH: Allow to register standalone figures with pyplot +* :ghpull:`29742`: DOC: Explain how to start the mainloop after show(block=False) +* :ghpull:`29502`: CI: remove xfail on OSX + tk due to issues in image +* :ghpull:`30514`: Prepare for MetaFont/PK font support. +* :ghpull:`30536`: DOC: Cleanup/restructure PR guidelines +* :ghpull:`30405`: ENH: Scroll to zoom +* :ghpull:`30530`: Bump the actions group across 1 directory with 10 updates +* :ghpull:`30532`: MNT: Change default name of ListedColormaps +* :ghpull:`30535`: Fix: pytest warning - GioUnix was imported without specifying version +* :ghpull:`30520`: pdf: Simplify Type 3 font character encoding +* :ghpull:`30387`: MNT: Refactor default violin KDE estimator +* :ghpull:`30462`: FIX: Mark shared Axes as stale when propagating adjustable +* :ghpull:`30507`: DOC: Clarify draft PR and move from ways to contribute to PR guidelines +* :ghpull:`30465`: removed test_image_cursor_formatting() +* :ghpull:`29939`: Parse {lua,xe}tex-generated dvi in dviread. +* :ghpull:`30510`: Update syntax for PR welcome workflow +* :ghpull:`30000`: Implement text shaping with libraqm +* :ghpull:`30408`: MNT/DOC: Deprecate anchor in Axes3D.set_aspect +* :ghpull:`30491`: merge up v3.10.6 +* :ghpull:`30475`: Fix spelling error in ``contains_branch_separately`` method name +* :ghpull:`30505`: Add Linux Foundation Health Score badge to README +* :ghpull:`30423`: Fix Line3DCollection with autolim=True for lines of different lengths +* :ghpull:`30479`: Clarify inset_locator.inset_axes demo. +* :ghpull:`30467`: Let ticklabels respect set_in_layout(False). +* :ghpull:`30478`: MNT: correct _replacer docstring +* :ghpull:`30471`: DOC: Fix text formatting of imshow_extent example +* :ghpull:`30469`: Deprecate redundant axes parameter to RadialLocator. +* :ghpull:`30384`: Add datetime test for ax.violin +* :ghpull:`30470`: No need to sanitize extrema in Colorizer.set_clim +* :ghpull:`30468`: Let triage_tests support test modules with only figure_equals tests. +* :ghpull:`30433`: Use standard property alias machinery in contour(). +* :ghpull:`30459`: DOC: simplify hat graph example +* :ghpull:`30456`: DOC: Correct a typo: confuzzlment -> confuzzlement +* :ghpull:`30455`: DOC: Fix typo in axes docstring +* :ghpull:`30454`: Added handling for undetermined home directory +* :ghpull:`30453`: DOC: Fix missing references on text-overhaul branch +* :ghpull:`30401`: merge up v3.10.5 +* :ghpull:`30452`: DOC: Move capture_scroll What's new note to new directory +* :ghpull:`30403`: Add scroll capture functionality to WebAgg backend +* :ghpull:`29876`: MultiNorm class +* :ghpull:`30446`: Added hardcoded colormap attributes for type checker support +* :ghpull:`30441`: Bump github/codeql-action from 3.29.8 to 3.29.10 in the actions group +* :ghpull:`30328`: Fix legend ``labelcolor=‘linecolor’`` to handle various corner cases, e.g. step histograms and transparent markers +* :ghpull:`30440`: Document relative font sizes +* :ghpull:`30402`: Update release guide +* :ghpull:`30031`: merge up 3.10.3 +* :ghpull:`30425`: Remove outdated reference to matplotlibbaselinemarker in tex sources. +* :ghpull:`29358`: MNT: Registered 3rd party scales do not need an axis parameter anymore +* :ghpull:`30422`: DOC: remove some usages of None as explicit defaults +* :ghpull:`30304`: Move release related docs to new sub-folder +* :ghpull:`30416`: Bump the actions group across 1 directory with 7 updates +* :ghpull:`30404`: DOC: Scale axis parameter +* :ghpull:`30324`: Make PyFT2Font a subclass of FT2Font +* :ghpull:`30362`: {,Range}Slider: accept callable valfmt arguments +* :ghpull:`30226`: ENH: Add properties bottoms, tops, and position_centers to BarContainer +* :ghpull:`30398`: TST: Remove qt_core fixture +* :ghpull:`30396`: Fix the link to latest stable documentation +* :ghpull:`30382`: MNT: Remove explicit use of default value add_collection(..., autolim=True) +* :ghpull:`30383`: DOC: Simplify Line, Poly and RegularPoly example +* :ghpull:`29958`: ENH: ax.add_collection(..., autolim=True) updates view limits +* :ghpull:`30374`: TST: Make determinism test plots look less pathological +* :ghpull:`29716`: ENH: Add align parameter to broken_barh() +* :ghpull:`30284`: Fixed the overdeletion of source images for failing tests +* :ghpull:`30348`: Keep default minor log ticks if there's 1 major & 1 minor tick. +* :ghpull:`30273`: Fix mlab fallback for 32-bit systems +* :ghpull:`30143`: TYP: Make glyph indices distinct from character codes +* :ghpull:`29465`: ENH: Type the possible str legend locs as Literals +* :ghpull:`30375`: Fix highlighting of install docs. +* :ghpull:`30376`: Shorten setup of axes in simple_axis_pad demo. +* :ghpull:`30367`: Support passing xticks/yticks when constructing secondary_axis. +* :ghpull:`30368`: Switch get_grid_info to take a single Bbox as parameter. +* :ghpull:`29993`: Trigger events via standard callbacks in widget testing. +* :ghpull:`30363`: Register 'avif' format when available in Pillow +* :ghpull:`29890`: Show subprocess stdout and stderr on pytest failure +* :ghpull:`30373`: Mnt/test qol improvements +* :ghpull:`30359`: ENH: Allow tuple for borderpad in AnchoredOffsetbox +* :ghpull:`30366`: Cross-ref the two-scales and secondary-axes examples. +* :ghpull:`30349`: Axes can't set navigate_mode. +* :ghpull:`30347`: Small cleanups. +* :ghpull:`30322`: Deprecate setting text kerning factor to any non-None value +* :ghpull:`30332`: CI: Harden GHA configuration +* :ghpull:`30346`: MNT: Fix isort line length setting +* :ghpull:`30314`: [MNT] Typing: correct typing overloads for ````Figure.subfigures```` +* :ghpull:`30343`: Fix broken/deprecated documentation links in MEPs and testing guides +* :ghpull:`30330`: [fix] Spine.set_bounds() does not take parameter **None** as expected +* :ghpull:`30339`: MNT: Prefer capitalized logging levels +* :ghpull:`30340`: Bump the actions group with 2 updates +* :ghpull:`30302`: [MNT] Typing: Use Literal for set_loglevel +* :ghpull:`30001`: Include close matches in error message when key not found +* :ghpull:`30333`: FIX: cast Patch linewidth to float for dash scaling +* :ghpull:`30329`: Deprecate font_manager.is_opentype_cff_font +* :ghpull:`25573`: FIX: be very paranoid about checking what the current canvas is +* :ghpull:`30319`: Don't set a default size for FT2Font +* :ghpull:`29816`: Update FreeType to 2.13.3 +* :ghpull:`30317`: fix broken configobj link +* :ghpull:`30261`: [TYP] Add more literals to MarkerType +* :ghpull:`30312`: Replace deprecated imports +* :ghpull:`30315`: Fix link to pango +* :ghpull:`30272`: Log a warning if selected font weight differs from requested +* :ghpull:`30311`: Bump the actions group with 2 updates +* :ghpull:`30309`: Improve custom sphinx link redirect extension +* :ghpull:`30174`: FIX: Ensure Locators on RadialAxis are always correctly wrapped +* :ghpull:`30281`: Fix several minor typos +* :ghpull:`30275`: Create events type and update plt.connect and mpl_connect +* :ghpull:`30279`: fix(config): Correct invalid value for svg.fonttype in matplotlibrc +* :ghpull:`30134`: Add typing to AFM parser +* :ghpull:`30274`: ci: Fix image preload with multiple conflicts +* :ghpull:`30231`: ci: Preload existing test images from text-overhaul-figures branch +* :ghpull:`29115`: Use old stride_windows implementation on 32-bit builds +* :ghpull:`30235`: Don't expose private styles in style.available +* :ghpull:`30266`: DOC: fix artist see also sections +* :ghpull:`30258`: Clean up mypy & ruff config +* :ghpull:`30262`: Tweak docstrings of get_window_extent/get_tightbbox. +* :ghpull:`30239`: Upgrade to Visual Studio 2022 in appveyor.yml +* :ghpull:`30245`: Adjust logic in RcParams to allow for inheritance +* :ghpull:`30232`: Bump github/codeql-action from 3.29.0 to 3.29.2 in the actions group +* :ghpull:`30196`: agg: Replace facepair_t with std::optional +* :ghpull:`30200`: Add explicit signatures for pyplot.{polar,savefig,set_loglevel} +* :ghpull:`30178`: Abstract base class for Normalize +* :ghpull:`30220`: BUG: Include python-including headers first in src/ft2font.{cpp,h} +* :ghpull:`30199`: Add explicit getter / setter overloads for pyplot.{xlim,ylim} +* :ghpull:`30202`: Add explicit overloads for pyplot.{show,subplot} +* :ghpull:`29988`: Refactoring: Removing axis parameter from scales +* :ghpull:`30082`: Simplify dviFontInfo layout in backend pdf. +* :ghpull:`30163`: Prepare to turn matplotlib.style into a plain module. +* :ghpull:`30206`: Use collections.deque to store animation cache data. +* :ghpull:`29481`: Support individual styling of major and minor grid through rcParams +* :ghpull:`28764`: Fix argument types in examples and tests +* :ghpull:`30197`: DOC: Remove last userdemo example +* :ghpull:`30191`: Simplify RendererAgg::draw_markers buffers +* :ghpull:`30188`: Fixed incomplete deletion of all images that have passed tests before upload +* :ghpull:`30168`: Remove fallback code for glyph indices +* :ghpull:`29102`: TST: Calculate RMS and diff image in C++ +* :ghpull:`30145`: Remove ttconv backwards-compatibility code +* :ghpull:`30181`: Bump the actions group with 3 updates +* :ghpull:`28187`: Add a filename-prefix option to the Sphinx plot directive +* :ghpull:`30154`: Bump github/codeql-action from 3.28.18 to 3.28.19 in the actions group +* :ghpull:`30054`: Fixed an off-by-half-pixel bug in image resampling when using a nonaffine transform (e.g., a log axis) +* :ghpull:`30150`: Update font-related documentation +* :ghpull:`29199`: Fix center of rotation with rotation_mode='anchor' +* :ghpull:`30153`: Throw exception when alpha is out of bounds +* :ghpull:`30151`: Fix typo in backend_ps.py comment: change 'and them scale them' to 'and then scale them' +* :ghpull:`30107`: Add example to histogram colorbar on galleries +* :ghpull:`20716`: Type-1 font subsetting +* :ghpull:`30067`: Remove deprecations: is_bbox and more +* :ghpull:`28560`: ENH: Add grouped_bar() method +* :ghpull:`30137`: BLD: Remove FreeType from Agg backend extension +* :ghpull:`29392`: Fill hatch in PDF backend +* :ghpull:`30130`: Make NavigationToolbar.configure_subplots return value consistent +* :ghpull:`30132`: DOC: Clarify that types in docstrings do not use formal type annotation syntax +* :ghpull:`30131`: DOC: Document the properties of Normalize +* :ghpull:`30112`: Update to docs with regards to colorbar and colorizer +* :ghpull:`30004`: Remove apply_theta_transforms argument +* :ghpull:`30070`: Deprecate point_at_t and document that a BezierSegment can be called +* :ghpull:`30121`: Clean up AFM code +* :ghpull:`30123`: Fix FT_CHECK compat with macOS 10.15 +* :ghpull:`30088`: Parse FontBBox in type1font. +* :ghpull:`30099`: Fix tight-bbox computation of HostAxes. +* :ghpull:`30102`: Simplify/improve error reporting from ft2font. +* :ghpull:`30113`: Bump scientific-python/circleci-artifacts-redirector-action from 1.0.0 to 1.1.0 in the actions group +* :ghpull:`30100`: Use fix-cm instead of type1cm. +* :ghpull:`30109`: DOC: expand petroff10 example to include 6- and 8- styles +* :ghpull:`30044`: Replace FT2Image by plain numpy arrays. +* :ghpull:`30097`: remove point troubling regex +* :ghpull:`30090`: Simplify some Sphinx tests +* :ghpull:`30061`: Move test data into a single subdirectory +* :ghpull:`30085`: DOC: add API docs content guidelines to api docs instructions +* :ghpull:`30084`: DOCS: add plot types content guidance to docs +* :ghpull:`30087`: DOC: Add petroff6 and petroff8 to 'Named color sequences' example +* :ghpull:`30080`: Bump the actions group with 3 updates +* :ghpull:`30065`: ENH: Add Petroff 6 and 8 color cycle style sheets +* :ghpull:`30077`: Fix deprecated attribute name in backend_pdf. +* :ghpull:`30069`: Close star polygons +* :ghpull:`30062`: Add 3D scatter test for cmap update +* :ghpull:`30066`: Remove get_bbox_header +* :ghpull:`30045`: CI: try running the precommit hooks on GHA +* :ghpull:`29910`: DOC: add warnings about get_window_extent and BboxImage +* :ghpull:`30032`: Add Matplotlib Journey online course to external resources +* :ghpull:`30055`: Renamed an RST file to remove a leading space in its filename +* :ghpull:`30049`: DOC: consolidate version switcher guidance +* :ghpull:`30050`: DOC: Additional tip to exclude undesired matches in GitHub code search +* :ghpull:`30005`: Remove cm.get_cmap +* :ghpull:`30048`: DOC: version switcher update on release +* :ghpull:`30047`: Update version switcher for 3.10.3 +* :ghpull:`30036`: Remove cutout for missing font file in PdfFile._embedTeXFont. +* :ghpull:`29847`: ci: restrict 'pygobject-ver' for Ubuntu 22.04 jobs +* :ghpull:`30030`: Add "sans" alias to rc() to allow users to set font.sans-serif +* :ghpull:`30040`: Improve usetex and pgf troubleshooting docs. +* :ghpull:`30037`: Update top message matplotlibrc file +* :ghpull:`30035`: Remove meson-python pinning +* :ghpull:`30006`: Enable linting of .pyi files +* :ghpull:`30020`: Micro-optimize _to_rgba_no_colorcycle. +* :ghpull:`30027`: Make PdfFile font-related attributes private. +* :ghpull:`29829`: Rework mapping of dvi glyph indices to freetype indices. +* :ghpull:`30023`: Remove unused ``_api`` import +* :ghpull:`30014`: Remove deprecated get_tick_iterator() +* :ghpull:`30015`: Expire deprecation of nth_coord arguments +* :ghpull:`30019`: FIX #30007: Raise ValueError when all wedge sizes are zero in ax.pie +* :ghpull:`30016`: Bump github/codeql-action from 3.28.16 to 3.28.17 in the actions group +* :ghpull:`30003`: DOC: missing word + add latex dep section +* :ghpull:`29341`: Type annotation add_subplot for projection="3d" +* :ghpull:`29764`: added latex requirements from fedora spec +* :ghpull:`29918`: DOC: Add descriptions to matplotlib.typing +* :ghpull:`27576`: Fix specifying number of levels with log contour +* :ghpull:`29879`: Adding elinestyle property to errorbar +* :ghpull:`29984`: FIX: Typing of FuncAnimation +* :ghpull:`29973`: Use inline lambdas to define most FT2Font properties. +* :ghpull:`29982`: Bump the actions group with 5 updates +* :ghpull:`29972`: Improve repr of mathtext internal structures; minor cleanup. +* :ghpull:`29356`: Add a last resort font for missing glyphs +* :ghpull:`29873`: Handled non finite values in ax.pie - issue #29860 +* :ghpull:`29916`: Bump the actions group with 2 updates +* :ghpull:`27183`: Fix behaviour of Figure.clear() for SubplotParams +* :ghpull:`29954`: Simplify ``colored_line()`` implementation in Multicolored lines example +* :ghpull:`29956`: MNT: make signature of GridSpec.update explicit +* :ghpull:`29203`: Fixed imsave() saving incorrect color map +* :ghpull:`29946`: Changed "Autoscaling axes" to "Autoscaling axes on user guide page for issue & closes #29906 +* :ghpull:`29948`: Check Axes/Figure import paths in boilerplate.py +* :ghpull:`29904`: API: bump minimum supported version of Python and numpy +* :ghpull:`29945`: Doc fixed aspect colorbar +* :ghpull:`29944`: DEV: have ruff check blank-line counts +* :ghpull:`29923`: Fix signature of disabled draw methods +* :ghpull:`29614`: add detail to doc string in Line3DCollection +* :ghpull:`29843`: Fix loading of Type1 "native" charmap. +* :ghpull:`29911`: Bump pre-commit versions +* :ghpull:`29892`: FIX: make_image should not modify original array +* :ghpull:`29905`: Remove hatchcolors parameter from draw_quad_mesh +* :ghpull:`29898`: backend_bases.pyi: ``@overload`` ``FigureCanvasBase.mpl_connect()`` for different event types +* :ghpull:`29745`: Use PEP8 style method and function names from pyparsing +* :ghpull:`29762`: Use ruff instead of flake8 to check PEP8 +* :ghpull:`29885`: Bump github/codeql-action from 3.28.13 to 3.28.14 in the actions group +* :ghpull:`29592`: DOC: Remove simple_legend examples from User Demo +* :ghpull:`29875`: DOC: Improve description of background/bbox handling for Text +* :ghpull:`29612`: ENH: Support units when specifying the figsize +* :ghpull:`29833`: TST: remove (most) text from constrained layout tests +* :ghpull:`29870`: doc: a grammatical error in pyplot comment +* :ghpull:`29831`: Inline _calc_extents_from_path. +* :ghpull:`29851`: Do not extraneously clip 3D plots +* :ghpull:`29846`: ci: cleanup: remove stale/outdated version range restrictions +* :ghpull:`29841`: Bump the actions group with 2 updates +* :ghpull:`29850`: MNT: Use Gcf.destroy(manager) instead of Gcf.destroy(manager.num) +* :ghpull:`29765`: ci: Introduce ubuntu-24.04 to restore GTK test coverage with recent PyGObject versions +* :ghpull:`29838`: Switch Tfm metrics to TrueType-compatible API. +* :ghpull:`29783`: Fix log scaling for pcolor and pcolormesh +* :ghpull:`29832`: MNT: expire legend-related deprecations +* :ghpull:`29044`: Add hatchcolor parameter for Collections +* :ghpull:`29828`: Improve output of dvi debug parsing. +* :ghpull:`29798`: Ensure polar plot radial lower limit remains at 0 after set_rticks + plot +* :ghpull:`29830`: Fix git fetch on development workflow +* :ghpull:`29776`: Filter images in premultiplied alpha mode. +* :ghpull:`29821`: Tweak minimal checks for GUI binding installs. +* :ghpull:`29808`: ENH: set default color cycle to named color sequence +* :ghpull:`29817`: Prepare for {xe,lua}tex support in usetex. +* :ghpull:`27972`: Fix ngrids support in axes_grid.Grid(). +* :ghpull:`29804`: replace quansight-labs/setup-python with actions/setup-python +* :ghpull:`29800`: Bump the actions group with 6 updates +* :ghpull:`29083`: DOC: Update page to note installation for ninja library +* :ghpull:`29698`: Improve tick subsampling in LogLocator. +* :ghpull:`29701`: Bump the actions group across 1 directory with 7 updates +* :ghpull:`28352`: Add compilers to conda environment +* :ghpull:`29696`: ENH: Add support for per-label padding in bar_label +* :ghpull:`29582`: Add ``rasterized`` option to ``contourf`` +* :ghpull:`29759`: DOC: expand use of fun tag +* :ghpull:`29758`: DOC: consolidate tags +* :ghpull:`29756`: Consolidate color tags +* :ghpull:`29747`: Revert "NEP 29 > SPEC 0 in dependency policy" +* :ghpull:`29744`: NEP 29 > SPEC 0 in dependency policy +* :ghpull:`29700`: merge up v3.10.1 +* :ghpull:`26774`: Connect the Animation event source callback in the constructor. +* :ghpull:`29729`: DOC: Improve What's new entry description +* :ghpull:`29718`: Update version switcher for 3.10.1 +* :ghpull:`29602`: MNT: Reduce the use of get_xticklabels() in examples +* :ghpull:`29705`: DOC: improve dev install docs +* :ghpull:`29644`: [Doc] Added images of hatches to hatch API page +* :ghpull:`29697`: MNT: remove ``plot_date`` +* :ghpull:`29690`: Add test cases for patch.force_edgecolor behavior with facecolor="none" +* :ghpull:`29558`: Consolidate align_labels_demo and align_ylabels gallery examples +* :ghpull:`29660`: fix: broken link +* :ghpull:`29639`: Bump the actions group across 1 directory with 7 updates +* :ghpull:`29620`: DOC: Add tip how to use GitHub code search to estimate the impact of a deprecation +* :ghpull:`29613`: doc: add link to analytics page +* :ghpull:`29593`: Fix tick_params() label rotation mode +* :ghpull:`29589`: DOC: Minor example cleanup +* :ghpull:`29580`: DOC: More cleanup of missing-references.json +* :ghpull:`29581`: Use functools.cache instead of lru_cache to establish singletons. +* :ghpull:`29566`: DOC: Remove invalid link in Communication Guide +* :ghpull:`29565`: Remove rcParams deprecation machinery +* :ghpull:`29561`: DOC: Document _CollectionWithSizes +* :ghpull:`29569`: Ignore ImageMagick deprecation of "convert" command. +* :ghpull:`29574`: 3D depthshade what's new plot +* :ghpull:`29052`: FIX: Checks for (value, color) tuples in LinearSegmentedColormap.from_list +* :ghpull:`29556`: Spacing for description of linecolor +* :ghpull:`28784`: Improve fallback font export tests +* :ghpull:`28968`: Implement xtick and ytick rotation modes +* :ghpull:`29450`: Remove some unused resample code +* :ghpull:`29503`: Improve error message for shape mismatches in barh function +* :ghpull:`29553`: DOC: update active social media list +* :ghpull:`27304`: Allow user to specify colors in violin plots with constructor method +* :ghpull:`29287`: Fix depth shading on 3D scatterplots +* :ghpull:`29398`: Speed up Collection.set_paths +* :ghpull:`29525`: Add new method Colormap.with_alpha() +* :ghpull:`29537`: Fix: Ensure ScalarFormatter.set_useOffset properly distinguishes betw… +* :ghpull:`29533`: Minor cleanups. +* :ghpull:`29397`: 3D plotting performance improvements +* :ghpull:`29529`: MNT: Deprecate other capitalization than "None" in matplotlibrc +* :ghpull:`29526`: DOC: better separation of codespace instructions +* :ghpull:`29486`: FIX: Make stem() baseline follow the curvature in polar plots +* :ghpull:`29460`: ENH: Add bad, under, over kwargs to Colormap +* :ghpull:`29435`: Fix ``plot_wireframe`` with nonequal ``rstride``, ``cstride``, plus additional speedups +* :ghpull:`29491`: Bump the actions group across 1 directory with 2 updates +* :ghpull:`29375`: Doc: document pending deprecation procedure +* :ghpull:`29497`: ci: Fix cache key for Matplotlib data +* :ghpull:`29473`: CI: add py312 and py313 on windows on azure to test matrix +* :ghpull:`29477`: ci: Add an ARM Linux test workflow +* :ghpull:`29372`: DOC / BUG: Fix savefig to GIF format with .gif suffix +* :ghpull:`29028`: Update colormap usage documentation to prioritize string colormap names +* :ghpull:`29461`: DOC: Use color specification reference in matplotlib.colors docs +* :ghpull:`29438`: ft2font: Avoid undefined enum values +* :ghpull:`29463`: Fix dead links in dev workflow docs +* :ghpull:`29464`: DOC: Add missing examples for legend outside positions +* :ghpull:`29433`: Remove erroneous statement in multipage PDF example +* :ghpull:`29441`: DOC: Rename Twitter to X +* :ghpull:`29399`: plot_wireframe plotting speedup +* :ghpull:`29325`: Propagate Axes class and kwargs for twinx and twiny +* :ghpull:`29424`: MNT: Turn Axes._axis_map into a static dict instead of a property +* :ghpull:`29427`: BUG: Fix regression with set_hatchcolor +* :ghpull:`29419`: Merge v3.10.x into main +* :ghpull:`29413`: [pre-commit.ci] pre-commit autoupdate +* :ghpull:`29415`: Bump the actions group across 1 directory with 5 updates +* :ghpull:`29338`: Use set_window_title rather than set_label to set title of webagg figure +* :ghpull:`29388`: FIX: get_tick_position() should disregard major/minor ticks that are not drawn +* :ghpull:`27327`: Update for checking whether colors have an alpha channel +* :ghpull:`29405`: DOC: Clearer wording for the installation of external dependencies +* :ghpull:`29402`: Expand set_ticklabels warning +* :ghpull:`29400`: Fix/Suppress more missing references +* :ghpull:`29394`: Tick rendering speedups +* :ghpull:`29386`: MNT: Remove ``*args`` for ``OffsetBox.__init__()`` +* :ghpull:`28104`: Separates edgecolor from hatchcolor +* :ghpull:`29377`: DOC: change wording on new contributor path +* :ghpull:`29376`: API: bump the minimum version of pillow +* :ghpull:`29333`: ENH: Streamplot control for integration max step and error +* :ghpull:`29342`: MNT: Warn on using pixel marker for scatter() +* :ghpull:`29344`: MNT: Coerce LineStyleType strings to Literal +* :ghpull:`29354`: Use _val_or_rc in more places +* :ghpull:`29360`: DOC: update switcher for 3.10 +* :ghpull:`29174`: ``indicate_inset`` transform support +* :ghpull:`27551`: Move axisartist towards untransposed transforms (operating on (N, 2) arrays instead of (2, N) arrays). +* :ghpull:`24714`: Improve handling of degenerate jacobians in non-rectilinear grids. +* :ghpull:`29343`: MNT: Discourage alternate strings for 'none' linestyle +* :ghpull:`29054`: Label log minor ticks if only one log major tick is drawn. +* :ghpull:`29346`: DOC: fix typos +* :ghpull:`29340`: FIX: pass renderer through adjust_bbox +* :ghpull:`29345`: MNT: Remove duplicate assignment +* :ghpull:`29329`: CI: allow pandas install to fail on nightly test run +* :ghpull:`29322`: DOC: Add [*Discouraged*] prefix to summary lines +* :ghpull:`25870`: Adds error handling around install_repl_displayhook +* :ghpull:`29303`: DOC: Enhance documentation on discouraged API +* :ghpull:`29280`: Apply some modernization to C++ extensions +* :ghpull:`23085`: Update art3d.py to address strange behavior of depthshading on 3D scatterplots with close points +* :ghpull:`29215`: added venv to gitignore +* :ghpull:`29257`: fix typo +* :ghpull:`28775`: DOC: manually placing images example +* :ghpull:`29222`: TST: Simplify parts of animation tests +* :ghpull:`29220`: DOC: Set stable version to 3.9.3 +* :ghpull:`29214`: Fix typo in _LazyTickList class comment (lis -> list) +* :ghpull:`29171`: ci: Remove Linux & macOS from Azure +* :ghpull:`29187`: DOC: verify your changes +* :ghpull:`29184`: Minor tweaks to image docs. +* :ghpull:`29172`: Minor cleanups to docstrings, comments, and error messages. +* :ghpull:`29155`: Delay warning for deprecated parameter 'vert' of box and violin +* :ghpull:`27617`: Add new num_arrows option to streamplot +* :ghpull:`29135`: Deprecate ListedColormap(..., N=...) parameter +* :ghpull:`29147`: Simplify synthetic event generation in interactive pan/zoom tests. +* :ghpull:`29150`: TST: Run macosx backends in a subprocess +* :ghpull:`29066`: Check pressed mouse buttons in pan/zoom drag handlers. +* :ghpull:`29087`: DOC: escape broken cross links +* :ghpull:`29127`: MNT: Refactor matplotlib.colors.from_levels_and_colors() +* :ghpull:`29125`: Make ListedColormap.monochrome a property +* :ghpull:`29074`: Add "standard" Axes wrapper getters/setters for Axis invertedness. +* :ghpull:`29078`: Document how to discourage API +* :ghpull:`29079`: DOC: Replaced colormap for colorblindness +* :ghpull:`29077`: DOC: Replaced green with blue for colorblindness + +Issues (246): + +* :ghissue:`14235`: Add \underline to mathtext? +* :ghissue:`31462`: [Bug]: Errorbar plot on log-scaled Axes sets incorrect automatic lower limits +* :ghissue:`30859`: [Bug]: ax.relim() ignores scatter artist +* :ghissue:`31523`: [Bug]: twinx() and twiny() crash with cryptic errors on 3D axes +* :ghissue:`26901`: [ENH]: Remove ``canvas.draw`` from ``widgets.Cursor.onmove`` +* :ghissue:`30831`: [Bug]: AttributeError: 'TimedAnimation' object has no attribute '_framedata' +* :ghissue:`31513`: [Bug]: Flaky test_contour.py::test_labels on minver CI +* :ghissue:`24716`: [TST]: Add classic style to all old image tests. +* :ghissue:`28488`: [ENH]: Provide a way to avoid subcommands on import. +* :ghissue:`30413`: [MNT]: c++11 narrowing error when building for 32 bit targets +* :ghissue:`31122`: [ENH]: Give control whether twinx() or twiny() overlays the main axis +* :ghissue:`4822`: Light font variants cannot be accessed by common name +* :ghissue:`21409`: [Bug]: twinx and twiny ignores previous set_position +* :ghissue:`31404`: [Bug]: Crash when removing contour set after removing contour labels +* :ghissue:`30365`: [Bug]: Type hints for xy and xycoords in annotate are too strict +* :ghissue:`13044`: Center of rotation for text with rotation_mode='anchor' +* :ghissue:`29253`: [Bug]: Numbers in words not italic +* :ghissue:`31220`: Should we use font metrics for line height instead of "lp"? +* :ghissue:`22172`: [Bug]: \genfrac has bad spacing with (high) custom ruler +* :ghissue:`18389`: Vertical positioning in mathtext fraction rendering could be improved +* :ghissue:`18086`: sub/superscripts should be moved further from the baseline following large delimiters +* :ghissue:`3135`: Please add support for ttc font files (PDF/PS output) +* :ghissue:`16566`: OTF feature support (alternate figure styles, etc.) +* :ghissue:`20842`: [MNT]: Please update freetype version +* :ghissue:`8765`: Indic Script labels not rendered correctly +* :ghissue:`2071`: matplotlib can't handle "newer" TrueType fonts +* :ghissue:`23082`: [Bug]: Font rendering bug for Devanagari text +* :ghissue:`29357`: [Bug]: Incorrect rendering of Abugida fonts on Matplotlib visualization +* :ghissue:`29806`: [Feature Request] Proper Arabic Language Support in Matplotlib Plots +* :ghissue:`5210`: Unexpected replacement of \right) with exclamation point in MathTextParser output +* :ghissue:`9681`: Determine if ``hinting_factor`` setting can be dropped +* :ghissue:`21797`: [Bug]: Math fonts (Type 3) incorrectly embedded in PDF? +* :ghissue:`31464`: [Doc]: finding the simple example +* :ghissue:`31454`: [Doc]: Amend AI policy by a concrete list of dos and don’ts +* :ghissue:`31337`: wording questions +* :ghissue:`31406`: [ENH]: [Bug]: secondary_xaxes().set_xlim/xbound should warn or raise that it is ineffective +* :ghissue:`31400`: [ENH]: Support partial figsize setting +* :ghissue:`26620`: [Doc]: Improve legend loc and bbox_to_anchor documentation +* :ghissue:`31369`: Dead link in colormap docs [Ware] +* :ghissue:`31344`: [Bug]: Adding contour labels affects the shape of filled contours +* :ghissue:`31286`: [MNT]: Scale ``val_in_range`` method +* :ghissue:`30651`: [MNT]: Add copyright information for google's "turbo" colormap? +* :ghissue:`28298`: [Bug]: set linestyle='dashed' raise error with quiver and legend +* :ghissue:`31302`: ``stairs`` with dashed linestyle and fill=True raises ValueError +* :ghissue:`27870`: [ENH]: out-of-tree Pyodide builds in CI for Matplotlib +* :ghissue:`31164`: [MNT]: Adopt Scikit Learn's autoclose bot +* :ghissue:`31320`: [DOC]: Using matplotlib.pyplot.pcolormesh with shading='flat' +* :ghissue:`31247`: [Bug]: Changing limits by setting ticks does not emit "x/ylim_changed" +* :ghissue:`18159`: Add zoom_factory to matplotlib - where to put? +* :ghissue:`31235`: [Doc]: bad rendering of matplotlib.pyplot.quiver docs +* :ghissue:`31126`: [Bug]: FigureCanvasTkAgg renders clipped/oversized when embedded in layout-managed container on Windows HiDPI +* :ghissue:`15313`: star (*) symbol in text box cuts off bottom of text when saved +* :ghissue:`31182`: [Bug]: ``ax.hist()`` fails on sequence of timedeltas due to comparison with ``np.inf`` +* :ghissue:`31256`: [ENH]: Extend semilogx, etc to 3D +* :ghissue:`209`: 3D scatter plots don't work in logscale +* :ghissue:`23306`: [ENH]: allow passing a function to ``CallbackRegistry.disconnect`` +* :ghissue:`28766`: [Bug]: Alignment of minus sign when using LaTeX +* :ghissue:`31093`: [ENH]: Modifier key to discretize rotations for 3D plots +* :ghissue:`31194`: [ENH]: add ``errorbar.capthick`` and ``errorbar.elinewidth`` to mplstyle +* :ghissue:`31221`: [Doc]: ticks/ticklabels_rotation example should mention rotation_mode="xtick"/"ytick" +* :ghissue:`20779`: [ENH]: move .matplotlib folder from %USERPROFILE% on Windows +* :ghissue:`31225`: [Bug]: set_edgecolor(None) cannot recover the default style after changing the edge color of wedges with hatches +* :ghissue:`26092`: [Bug]: alpha array-type not working with RGB image in imshow() +* :ghissue:`31009`: [Bug]: Large pixels may overlap when using imshow() +* :ghissue:`31127`: [Doc]: quiver 3d does not support "mid" as an alias for "middle", but quiver 2d does +* :ghissue:`30848`: [MNT]: Should we request contributors to declare usage of AI? +* :ghissue:`25914`: [Doc]: replace usages of ``.imread`` with PIL.Image.open +* :ghissue:`30934`: [ENH]: Implement gapcolor for patch edges +* :ghissue:`24499`: [Doc]: Transformation tutorial uses outdated description for polar transform +* :ghissue:`31149`: [ENH]: Improve compatibility with array-like objects implementing __array__ (e.g. MLX arrays) +* :ghissue:`31135`: [Bug]: Setting figure for polar axes breaks the polar coordinates +* :ghissue:`28793`: ``patheffects.SimpleLineShadow`` calling non-existent ``get_foreground`` method from GraphicsContextBase +* :ghissue:`30658`: [MNT]: First contributor workflow fails for first contributors +* :ghissue:`19299`: wide mathtext accents are mis-centered +* :ghissue:`31086`: [Bug]: Colorbar get_ticks() return the incorrect array +* :ghissue:`2488`: Off-axes scatter() points unnecessarily saved to PDF when coloured +* :ghissue:`29551`: [Bug]: 3D tick label position jitter when rotating the plot view +* :ghissue:`30957`: [MNT]: Clarify the difference between Artist.set and Artist.update +* :ghissue:`30996`: [Doc]: ``contour`` and ``contourf`` levels default not specified +* :ghissue:`31003`: [ENH]: Add types for ``fig_kw`` argument in ``subplots`` +* :ghissue:`30417`: [ENH]: Support using datetimes as ``positions`` argument to violin(...) +* :ghissue:`30575`: [Bug]: Regression in widget behavior +* :ghissue:`23763`: [Bug]: Inconsistent rendering between backends when rendering Mathtext horizontal rule +* :ghissue:`23860`: [Bug]: Font weight of label cannot be overwritten from rcParams when using mathtext +* :ghissue:`29475`: [Doc]: interactive rebase instructions outdated? +* :ghissue:`29863`: [ENH]: Should we hide _preprocess_data from the stack trace? +* :ghissue:`30836`: [Bug]: Markers can be integers, but numpy integers fail +* :ghissue:`22231`: [Bug]: Axes.grid(color) ignores alpha +* :ghissue:`14143`: imshow pixel boundaries wrong when zoomed in +* :ghissue:`1441`: Misalignment imshow vs. grid lines +* :ghissue:`30882`: [Bug]: Flaky tests with "Python 3.11 on ubuntu-22.04 (Minimum Versions)" +* :ghissue:`27590`: [Bug]: Qt5 backend icons should be white when macOS in dark mode +* :ghissue:`23531`: [Doc]: Documentation of rc parameters could be improved +* :ghissue:`30559`: [ENH]: Backend versioning +* :ghissue:`30917`: [Bug]: TimerAsyncio does not work with Python 3.14 +* :ghissue:`30709`: [Bug]: Mismatch in documented default behaviour of pcolormesh 'snap' +* :ghissue:`30463`: [Doc]: Two sources of a gallery figure for normal and high-DPI screen are different +* :ghissue:`28983`: [Doc]: outdated links for violin/boxplot +* :ghissue:`30857`: [Bug]: ValueError: The 'color' keyword argument must have one color per dataset +* :ghissue:`29332`: [ENH]: Typing: broaden acceptable floats +* :ghissue:`23633`: [MNT]: Deprecated / discourage less used Axes methods forwarding to Axis methods +* :ghissue:`21496`: [MNT]: MultiCursor should not take canvas as first parameter +* :ghissue:`30563`: [Bug]: 3D text does not respect rotation to make it parallel with a given zdir axis +* :ghissue:`27969`: [ENH]: Please add ``matplotlib.patches.RoundedRectangle`` +* :ghissue:`29319`: [Bug]: Legend with location set to ‘best’ overlaps with the title when the titles is moved down +* :ghissue:`28513`: [Bug]: Segfault when using ``close_event`` with macosx backend and tk +* :ghissue:`30840`: [MNT]: ``LocationEvent.modifiers`` missing in type stub +* :ghissue:`30770`: [Bug]: Bug / Inconsistency: Title Format Lost After Interactive Editing +* :ghissue:`30673`: [ENH]: Add custom hatch styling to grouped_bar +* :ghissue:`30804`: [Bug]: Stackplot does not pass ``facecolor(s)`` correctly to fill_between +* :ghissue:`30537`: Permanent solution for GioUnix warning +* :ghissue:`27224`: [Bug]: pickling and unpickling hidpi a qt figure that has been already shown doubles its physical size +* :ghissue:`26380`: [Bug]: DPI keeps doubling when creating a new MatPlotLib QtWidget in qt6 +* :ghissue:`20415`: figure.raise_window keyword produces inconsistent results +* :ghissue:`18985`: Why does setting imshow(animated=True) still show an image? +* :ghissue:`22831`: [Doc]: Arguments of FFMpegFileWriter not clear. +* :ghissue:`30796`: [Doc]: Information about deprecated colormaps missing from recent versions of the documentation +* :ghissue:`7059`: Decoupling hatch from edges +* :ghissue:`30744`: [Bug]: axis3d.Axis.get_tightbbox() is not including the offset_text +* :ghissue:`30767`: [ENH]: Add rcParams for the width of the legend's box edge +* :ghissue:`30472`: [Bug]: layout=compressed conflict with suptitle +* :ghissue:`23998`: Labels for PatchCollection do not show +* :ghissue:`28889`: [Doc]: Reintroduce glossary for matplotlib terms and concepts +* :ghissue:`22402`: [Doc]: Quiver docstring incorrectly claims that only ``UVC`` can be set +* :ghissue:`19338`: Allow option to display absolute values for pie chart +* :ghissue:`30664`: [MNT]: Declare table() to be not further developed +* :ghissue:`30764`: [Bug]: Hexbin with bins='log' doesn't handle zeros as described +* :ghissue:`30439`: [Doc]: Link AI policy on contributing page +* :ghissue:`30740`: [ENH]: Support caption for code block in sphinx plot directive +* :ghissue:`30695`: [Bug]: bbox_inches='tight' works differently when ax.plot() have markers +* :ghissue:`30257`: [MNT] [TYPING]: Use of Literal +* :ghissue:`20724`: ToolHandles/ToolLineHandles could set the mouse cursor when hovered over or active +* :ghissue:`20554`: Remove discussion of jupyter backends from image tutorial +* :ghissue:`28827`: [Bug]: FontProperties objects are not deleted when using fig.savefig +* :ghissue:`30644`: [Doc]: Stable docs reporting as unstable +* :ghissue:`30613`: [Bug]: violin's default alpha no longer persists +* :ghissue:`22197`: [Bug]: TwoSlopeNorm behaves like CenteredNorm +* :ghissue:`30522`: [MNT]: PR Greeting GHA not working +* :ghissue:`30574`: [Bug]: Unicode symbols encoded with ``\u....`` with mathtext raise ParseFatalException +* :ghissue:`27190`: [Doc]: clarify when and how to use boilerplate.py +* :ghissue:`26739`: Write a separate doc-string for Line3DCollection +* :ghissue:`19956`: Native support for showing OOP-created figures +* :ghissue:`28412`: [ENH]: Zoom in/out on rolling the mouse wheel +* :ghissue:`30525`: [Bug]: Pipeline fails with "GioUnix was imported without specifying a version first" +* :ghissue:`30436`: [Doc]: new contributor guidance on draft PRs +* :ghissue:`30364`: [MNT]/[DOC]: Look into Axes3D.set_aspect ``anchor`` and ``adjustable`` arguments +* :ghissue:`30474`: [Bug]: Typo in method name: contains_branch_separately +* :ghissue:`30418`: [Bug]: error using ``add_collection3d`` of ``Line3DCollection`` with ``autolims=True`` and lines containing different numbers of points +* :ghissue:`30263`: [ENH]: Allow ignoring x-extent (but not y-extent) of xticklabels when computing axes extents (e.g. for geometry manager) +* :ghissue:`30296`: [MNT]: Deprecate the axes parameter to RadialLocator +* :ghissue:`29774`: [Bug]: triage_tests.py is brittle against failures in test modules that have only check_figures_equal test +* :ghissue:`29349`: [MNT]: Remove axis parameter from scales +* :ghissue:`1963`: Singular keyword arguments in contour don't raise exceptions +* :ghissue:`30449`: [Bug]: Config directory location finder doesn't account for the home directory being undetermined. +* :ghissue:`30438`: [Bug]: missing stubs for ``plt.cm`` (a.k.a. ``matplotlib.pyplot.cm``) +* :ghissue:`30298`: [Bug]: Legend kwarg ``labelcolor='linecolor'`` not working properly when ``facecolor`` is ``'None'`` +* :ghissue:`30437`: [Doc]: Clarification of relative font sizes +* :ghissue:`30400`: [Bug]: Megabyte-level memory leak when using imshow() in a loop +* :ghissue:`29957`: [ENH]: add_collection(..., autolim=True) should update view limits as well +* :ghissue:`22720`: [MNT]: Generalize widget mouse testing +* :ghissue:`28809`: [ENH]: Support avif as output format +* :ghissue:`30331`: [ENH]: inset_axes has borderpadding, but not x/y individually. +* :ghissue:`29300`: [Bug]: Background of rotated png is rendered black +* :ghissue:`30323`: [MNT]: validate linewidth +* :ghissue:`25572`: [Bug]: Artist.remove() isn't fully removing it from figure +* :ghissue:`30325`: [Bug]: fig.savefig throws error after radiobutton axes is removed +* :ghissue:`15529`: Chinese font can``t change the weight +* :ghissue:`30164`: [Bug]: Removing spines in polar plot causes distortion of the plot +* :ghissue:`27232`: BUG: .notdef glyph has to be present in fonts in fontlist +* :ghissue:`14239`: rotated text does not align +* :ghissue:`23021`: [Bug]: Text rotation leads to characters being misplaced within their bounding boxes. Attempted solution provided. +* :ghissue:`30160`: [MNT]: pyplot type hints +* :ghissue:`13919`: Impossible to configure minor/major grid line style independently in rcParams +* :ghissue:`25800`: [MNT]: Remove the userdemo section in examples +* :ghissue:`24313`: [ENH]: API discussion for grouped bar charts +* :ghissue:`29722`: [MNT]: Upcoming version of ``pyparsing`` will start emitting ``DeprecationWarnings`` for legacy pre-PEP8 method and argument names +* :ghissue:`30026`: [Doc]: add histogram as colorbar example +* :ghissue:`127`: When text.usetex=True with pdf backend, full subset of latex fonts is embedded into pdf file +* :ghissue:`10034`: Hatching is rendered differently by agg, pdf and svg backends. +* :ghissue:`19832`: Positioning floating_axes.FloatingSubplot +* :ghissue:`29791`: [Bug]: Saving as an SVG and PDF produce different outputs with Latex characters, with wrong character sizing +* :ghissue:`28675`: [Bug]: ``multialignment='right'`` in ``ax.text()`` with ``path_effects`` breaks when using LaTeX package ``\usepackage[T1]{fontenc}`` +* :ghissue:`27654`: [MNT]: Use fix-cm rather than type1cm for LaTeX +* :ghissue:`30086`: Add petroff6 and petroff8 color cycles to named color sequences example +* :ghissue:`30060`: Add the 6 color and 8 color sequence for the Petroff color cycles +* :ghissue:`28750`: Followup documentation for petroff color sequence +* :ghissue:`18931`: 3D collections do not proper handle ``edgecolor='face'`` +* :ghissue:`2831`: Bug when saving to vector format (pdf, svg, eps) +* :ghissue:`30046`: [Doc]: Documentation of the stable version still prompts that it is an unstable development version +* :ghissue:`29844`: [MNT]: CI: pygobject fails to install during ubuntu-22.04 GitHub Actions jobs +* :ghissue:`30021`: [Bug]: Setting font.sans-serif is impossible by the intended way using matplotlib.rc because it contains a hyphen. +* :ghissue:`30007`: Axes.pie([0, 0]) crashes with “cannot convert float NaN to integer” when all slice sizes are zero +* :ghissue:`29334`: [Bug]: Type annotation for ``add_subplots`` has incorrect return type for ``projection="3d"`` +* :ghissue:`29681`: [ENH]: Add parameter 'error_linestyle' to plt.errorbar() +* :ghissue:`29960`: [Bug]: FuncAnimation function not typed properly +* :ghissue:`29860`: ``ax.pie()`` raises ``ValueError`` when input contains ``NaN`` +* :ghissue:`11059`: figure.clf() and subplots_adjust +* :ghissue:`29906`: [Doc]: Autoscaling Axes or Autoscaling Axis? +* :ghissue:`29921`: boilerplate.py seems to remove parameters +* :ghissue:`29938`: [ENH]: plt.colorbar add a colorbar which has the same height/width of original image +* :ghissue:`29891`: [Bug]: image alpha re-applied each draw? +* :ghissue:`29883`: [Bug]: Missing backcompat for backends not supporting hatchcolors in draw_quad_mesh +* :ghissue:`27588`: [ENH]: Add way to automatically fix flake8 errors +* :ghissue:`1369`: add rc param for centimeter support +* :ghissue:`29845`: [MNT]: CI: cleanup: remove stale/outdated version range restrictions +* :ghissue:`29749`: [Bug]: Unit tests: Ubuntu 22.04 lacks dependencies required for recent PyGObject versions +* :ghissue:`29615`: [Bug]: pcolormesh's default x/y range might break ``set_scale('log')`` +* :ghissue:`29528`: [Bug]: set_rticks makes polar autoscale move the origin away from zero +* :ghissue:`29799`: [ENH]: set default color cycle to named color sequence +* :ghissue:`29694`: [Bug]: LogLocator sometimes draws fewer ticks than it can +* :ghissue:`29746`: [Doc]: Add uv and pixi install instructions +* :ghissue:`29647`: [ENH]: Allow list of padding values for bar_label +* :ghissue:`27669`: [Doc]: documentation of how to properly rasterize output of contourf +* :ghissue:`29757`: [Doc]: duplicate tags +* :ghissue:`29753`: [Doc]: color and colormap tags +* :ghissue:`29720`: [Bug]: Inset Axes Failing for Geographic Plot +* :ghissue:`29712`: [Doc]: Stable version of documentation has unstable banner +* :ghissue:`27196`: [Doc]: List supported hatches and link to/embed hatch reference on hatches API page +* :ghissue:`29562`: [MNT]: Remove rcParams deprecation machinery +* :ghissue:`29042`: [Bug]: colors.LinearSegmentedColormap.from_list fails when using a ("", alpha) tuple +* :ghissue:`28951`: [ENH]: Better positioning of rotated tick labels +* :ghissue:`29474`: [ENH]: Show parameter names in error message for mismatched array sizes in bar() +* :ghissue:`27298`: [ENH]: Add color argument to violinplot constructor +* :ghissue:`22861`: [Bug]: 3D scatter plot flips alpha order depending on depth relative to camera +* :ghissue:`29532`: [Bug]: ScalarFormatter can't be forced to use an offset of 1 +* :ghissue:`16659`: Speeding up Axes3D.plot_surface 4-8x +* :ghissue:`29524`: [Doc]: Unclear how to compile ``c_internals`` in code space +* :ghissue:`29489`: [Bug]: Systematic test failures with ubuntu-22.04-arm pipeline +* :ghissue:`28915`: [Doc]: Preferred way of specifying colormaps via ``cmap`` +* :ghissue:`29305`: [Doc]: Dead link in dev workflow docs +* :ghissue:`28763`: [MNT]: ListedColormap inconsistencies +* :ghissue:`29428`: [Doc]: Multipage PDF: unclear which backend supports and which does not support attach_note() +* :ghissue:`29387`: [MNT]: Fix 3.10 release notes and merge up +* :ghissue:`27321`: [Bug]: The method for checking whether a color has an alpha value is outdated +* :ghissue:`29284`: [Bug]: ``get_ticklabels``/``set_ticklabels`` gives incorrect values in log plot +* :ghissue:`26074`: [ENH]: Different edgecolor and hatch color in bar plot +* :ghissue:`29313`: [DOC]: possible typos +* :ghissue:`27763`: [Bug]: colorbar doesn't register inset_axis as cax +* :ghissue:`23770`: [Bug]: crash due to backend issue in ipython session started explicitly with InteractiveShell +* :ghissue:`19017`: Formalize discouraged API (= softer deprecations) +* :ghissue:`22521`: [Bug]: X-Axis date label not rotated +* :ghissue:`29181`: [Doc]: locally testing changes +* :ghissue:`17740`: Multiple Arrows on Streamplots +* :ghissue:`19101`: support for ticks crossing axes in axisartist +* :ghissue:`24050`: No error message in matplotlib.axes.Axes.legend() if there are more labels than handles +* :ghissue:`7305`: RuntimeError In FT2Font with NISC18030.ttf Previous GitHub statistics diff --git a/doc/release/prev_whats_new/github_stats_3.10.9.rst b/doc/release/prev_whats_new/github_stats_3.10.9.rst new file mode 100644 index 000000000000..73d2785531b4 --- /dev/null +++ b/doc/release/prev_whats_new/github_stats_3.10.9.rst @@ -0,0 +1,103 @@ +.. _github-stats_3-10-9: + +GitHub statistics for 3.10.9 (Apr 23, 2026) +=========================================== + +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2026/04/23 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 10 issues and merged 34 pull requests. +The full list can be seen `on GitHub `__ + +The following 37 authors contributed 519 commits. + +* Aasma Gupta +* Aman Srivastava +* Antony Lee +* beelauuu +* Ben Root +* Christine P. Chai +* David Stansby +* dependabot[bot] +* Elliott Sales de Andrade +* G.D. McBain +* Greg Lucas +* hannah +* hu-xiaonan +* Ian Thomas +* Inês Cachola +* Jody Klymak +* Jouni K. Seppänen +* Khushi_29 +* Kyle Sunden +* Lumberbot (aka Jack) +* m-sahare +* N R Navaneet +* Nathan G. Wiseman +* Oscar Gustafsson +* Praful Gulani +* Qian Zhang +* Raphael Erik Hviding +* Raphael Quast +* Roman +* Ruth Comer +* saikarna913 +* Scott Shambaugh +* Steve Berardi +* Thomas A Caswell +* Tim Hoffmann +* Trygve Magnus Ræder +* Vikash Kumar + +GitHub issues and pull requests: + +Pull Requests (34): + +* :ghpull:`31556`: FIX: Inverted PyErr_Occurred check in enum type caster (_enums.h) +* :ghpull:`31078`: Backport PR #31075 on branch v3.10.x (Fix remove method for figure title and xy-labels) +* :ghpull:`31280`: Backport PR #31278 on branch v3.10.x (Fix ``clabel`` manual argument not accepting unit-typed coordinates) +* :ghpull:`31520`: Backport PR #31020 on branch v3.10.x (DOC: Fix doc builds with Sphinx 9) +* :ghpull:`31511`: Backport PR #31504 on branch v3.10.x (Re-order variants to prioritize narrower types) +* :ghpull:`31504`: Re-order variants to prioritize narrower types +* :ghpull:`31445`: Backport PR #31437: mathtext: Fix type inconsistency with fontmaps +* :ghpull:`31437`: mathtext: Fix type inconsistency with fontmaps +* :ghpull:`31411`: Backport PR #31323 on branch v3.10.x (FIX: Prevent crash when removing a subfigure containing subplots) +* :ghpull:`31421`: Backport PR #31420 on branch v3.10.x (Fix outdated Savannah URL for freetype download) +* :ghpull:`31420`: Fix outdated Savannah URL for freetype download +* :ghpull:`31418`: Backport PR #31401: BLD: Temporarily pin setuptools-scm<10 +* :ghpull:`31323`: FIX: Prevent crash when removing a subfigure containing subplots +* :ghpull:`31401`: BLD: Temporarily pin setuptools-scm<10 +* :ghpull:`31278`: Fix ``clabel`` manual argument not accepting unit-typed coordinates +* :ghpull:`31154`: Backport PR #31153 on branch v3.10.x (TST: Use correct method of clearing mock objects) +* :ghpull:`31153`: TST: Use correct method of clearing mock objects +* :ghpull:`31075`: Fix remove method for figure title and xy-labels +* :ghpull:`31036`: Backport PR #31035 on branch v3.10.x (DOCS: Fix typo in time array step size comment) +* :ghpull:`30986`: Backport PR #30985 on branch v3.10.x (MNT: do not assign a numpy array shape) +* :ghpull:`30985`: MNT: do not assign a numpy array shape +* :ghpull:`30971`: Backport PR #30969 on branch v3.10.x (DOC: Simplify barh() example) +* :ghpull:`30965`: Backport PR #30952 on branch v3.10.x (DOC: Tutorial on API shortcuts) +* :ghpull:`30964`: Backport PR #30960 on branch v3.10.x (SVG backend - handle font weight as integer) +* :ghpull:`30960`: SVG backend - handle font weight as integer +* :ghpull:`30924`: Backport PR #30910 on branch v3.10.x (DOC: Improve writer parameter docs of Animation.save()) +* :ghpull:`30870`: Backport PR #30869 on branch v3.10.x (FIX: Accept array for zdir) +* :ghpull:`30869`: FIX: Accept array for zdir +* :ghpull:`30860`: Backport PR #30858 on branch v3.10.x (DOC: reinstate "codex" search term) +* :ghpull:`30818`: Backport PR #30817 on branch v3.10.x (Update sphinx-gallery header patch) +* :ghpull:`30801`: Backport PR #30763 on branch v3.10.x (DOC: Add example how to align tick labels) +* :ghpull:`30791`: Backport PR #30788 on branch v3.10.8-doc (Fix typo in key-mapping for "f11") +* :ghpull:`30790`: Backport PR #30788 on branch v3.10.x (Fix typo in key-mapping for "f11") +* :ghpull:`30788`: Fix typo in key-mapping for "f11" + +Issues (10): + +* :ghissue:`31495`: Unavoidable warnings with pybind11 main branch +* :ghissue:`31433`: [MNT]: Mypy error +* :ghissue:`31340`: [Bug]: outdated savannah URL in subprojects/freetype-2.6.1.wrap +* :ghissue:`31319`: [Bug]: Crash when removing a subfigure with a subplot in a figure +* :ghissue:`27525`: [Bug]: clabel manual argument does not accept units +* :ghissue:`31112`: [TST] Upcoming dependency test failures +* :ghissue:`31073`: [Bug]: Crash when Removing Suptitle in a Figure with Constrained Layout +* :ghissue:`30981`: [TST] Upcoming dependency test failures +* :ghissue:`30868`: [Bug]: Axe3D text() method does not allow zdir=numpy.array(...) +* :ghissue:`21566`: [ENH]: set_horizontalalignment("right") on Y axis labels when yaxis.ticks_right() is used. diff --git a/tools/github_stats.py b/tools/github_stats.py index af0255fcefba..6e442d220180 100755 --- a/tools/github_stats.py +++ b/tools/github_stats.py @@ -28,6 +28,8 @@ PER_PAGE = 100 REPORT_TEMPLATE = """\ +.. redirect-from:: /users/github_stats + .. _github-stats: {title} From 7de4f498f45fe1efd1b6c37f6bb435fd201afafc Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 24 Apr 2026 14:18:03 -0400 Subject: [PATCH 005/291] REL: v3.11.0rc1 This is the first release candidate for the meso release 3.11.0. From 3217fe2227aae0db6c8da042fcecc70a55d92e2a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 24 Apr 2026 14:20:30 -0400 Subject: [PATCH 006/291] BLD: bump branch away from tag So the tarballs from GitHub are stable. From 9e11feb1580d72c0594d98bf6d5b7500e4299f9e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 24 Apr 2026 17:59:53 -0400 Subject: [PATCH 007/291] Backport PR #31563: LIC: remove carlogo 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 2638e3e82360b816cb539f4019a25b69fcab1108 Mon Sep 17 00:00:00 2001 From: Hannah Date: Tue, 28 Apr 2026 15:01:58 -0400 Subject: [PATCH 008/291] 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 0dc69de6e4a4379a676602cd964dd749e62e10d2 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 28 Apr 2026 17:16:47 -0400 Subject: [PATCH 009/291] Backport PR #31580: DOC: 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 010/291] 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 1c625168466e9384b73b34454dc58f2c14879ef3 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 011/291] Backport PR #31577: FIX: Polar Radial Tick Warnings Labels Bug --- 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 012/291] 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 013/291] 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 014/291] 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 015/291] 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 016/291] 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 017/291] 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 018/291] 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 019/291] 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 020/291] 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 014076e2126a05954904c46589d87b4957ace9de Mon Sep 17 00:00:00 2001 From: "Lumberbot (aka Jack)" <39504233+meeseeksmachine@users.noreply.github.com> Date: Thu, 30 Apr 2026 23:58:33 +0200 Subject: [PATCH 021/291] Backport PR #31588: Expire some missed deprecations from 3.9 (#31592) Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- doc/api/next_api_changes/removals/31588-ES.rst | 18 ++++++++++++++++++ lib/matplotlib/axes/_axes.py | 4 +--- lib/matplotlib/backend_bases.py | 14 -------------- lib/matplotlib/hatch.py | 2 +- lib/matplotlib/tests/test_axes.py | 16 ++++------------ 5 files changed, 24 insertions(+), 30 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): """ 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 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 022/291] 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 023/291] 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 024/291] 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 025/291] 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 026/291] 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 75754b55e8763c13805a1d07aba3064640b8907b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 1 May 2026 23:59:56 -0400 Subject: [PATCH 027/291] Backport PR #31600: Bump https://github.com/astral-sh/ruff-pre-commit from v0.15.11 to 0.15.12 --- .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 d066418ff55ecc15cf86f14cf8dcd4b05960c6cb Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 2 May 2026 00:03:33 -0400 Subject: [PATCH 028/291] Backport PR #31599: Bump the actions group with 2 updates --- .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 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 029/291] 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 f32cfc57a2c5bf0fbce2f75cd082ce0bfd2d5860 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 2 May 2026 01:51:57 -0400 Subject: [PATCH 030/291] Backport PR #31594: DOC: Explain how to selectively restore ticks that are removed by sharex --- .../subplots_axes_and_figures/subplots_demo.py | 14 ++++++++++++-- 1 file changed, 12 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..87f5ed6a3d34 100644 --- a/galleries/examples/subplots_axes_and_figures/subplots_demo.py +++ b/galleries/examples/subplots_axes_and_figures/subplots_demo.py @@ -141,8 +141,18 @@ # %% # 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 c19bfe85534b7524b682316e1ea5d3b03b5d2d67 Mon Sep 17 00:00:00 2001 From: beelauuu Date: Sat, 2 May 2026 12:28:58 -0400 Subject: [PATCH 031/291] 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 032/291] 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 033/291] 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 e0b3138096d267b6b7ffdbb6844aa2e68fac3b4e Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 4 May 2026 23:12:13 +0200 Subject: [PATCH 034/291] Backport PR #31608: Remove outdated comment re: implementation of hinting_factor. --- 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 035/291] 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 036/291] 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 037/291] 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 038/291] [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 56e15b53e4442c400f229d9d03660e2973f8813a 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 039/291] Backport PR #25478: [BUG] Fix alpha bug on 3D PathCollection plots. --- 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 2836d9b78787c4500beb931dad857888011b9a56 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 7 May 2026 11:38:18 +0200 Subject: [PATCH 040/291] Backport PR #31625: DOC: Inline ScalarMappable reStructuredText entries --- 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 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 041/291] 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 4786b8353739f66d4465bf770135e1c922baf2b6 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 7 May 2026 15:27:01 -0400 Subject: [PATCH 042/291] Backport PR #31621: Make Scale axis parameter handling more flexible --- 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 983dc8eeac9d925bbd005b3c6c9755fc0dd0a2bb Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 7 May 2026 15:56:37 -0400 Subject: [PATCH 043/291] 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 044/291] 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 f05f7a28c73d15a83566d633feba88f3fc1c6202 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 7 May 2026 16:19:22 -0400 Subject: [PATCH 045/291] Backport PR #31557: FIX: Added 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 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 046/291] 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 047/291] 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 048/291] 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 049/291] 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 050/291] 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 051/291] 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 34b1c9eebf50a59b3883dca1018ec4383b207eba Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Fri, 8 May 2026 13:24:08 +0100 Subject: [PATCH 052/291] Backport PR #31630: Restore PolarTransform(apply_theta_transforms) parameter --- 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 e00071c049fc508875e8b8201aae506c97f9e06e Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 8 May 2026 14:03:29 -0500 Subject: [PATCH 053/291] Backport PR #31634: Fix some font-related issues --- lib/matplotlib/_api/__init__.pyi | 4 +--- lib/matplotlib/_mathtext.py | 2 +- lib/matplotlib/backend_bases.pyi | 3 ++- lib/matplotlib/backends/_backend_pdf_ps.py | 5 +++-- lib/matplotlib/backends/backend_cairo.py | 5 ++++- lib/matplotlib/dviread.py | 2 +- lib/matplotlib/font_manager.pyi | 14 +++++++++++--- lib/matplotlib/ft2font.pyi | 3 ++- 8 files changed, 25 insertions(+), 13 deletions(-) diff --git a/lib/matplotlib/_api/__init__.pyi b/lib/matplotlib/_api/__init__.pyi index 0bcce210634f..aeefaa35ffaf 100644 --- a/lib/matplotlib/_api/__init__.pyi +++ b/lib/matplotlib/_api/__init__.pyi @@ -41,9 +41,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( 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/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() 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) 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 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 054/291] 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 da1f383890abc41317ebae1da19a77a7e24b7f14 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 8 May 2026 15:42:29 -0400 Subject: [PATCH 055/291] Backport PR #31628: 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 84d7bbeefee30eb62ed327b3135e22b899415415 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 8 May 2026 15:10:06 -0500 Subject: [PATCH 056/291] Backport PR #31638: Bump the actions group with 2 updates --- .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 057/291] 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 058/291] 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 b64ab85a316e5dfa67f3b05a7aa5d1e9d85dfb22 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 059/291] Backport PR #31609: DOC: Improve autoscaling and margin docs --- 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 060/291] 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 f821c45ba29e7735100621510205411521aa903e 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 061/291] Backport PR #31579: DOC: Document that bar() errorbars do not support individual coloring --- 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 76659ae2c83d..687bb4b48d3b 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2409,11 +2409,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 9d31ef590d944e2cc2416200557fed87d41e4f60 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 9 May 2026 21:13:59 -0400 Subject: [PATCH 062/291] Backport PR #31632: FIX: Prohibit special TeX chars in pgf metadata --- lib/matplotlib/backends/backend_pgf.py | 9 +++++++++ lib/matplotlib/tests/test_backend_pgf.py | 22 +++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 3205f294ab2d..36048fe016df 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -154,6 +154,15 @@ def _metadata_to_str(key, value): value = value.name.decode('ascii') 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( + 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 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 063/291] 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 064/291] 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 6dd1f7e114ecf30baa8ef2ede69a8c233e7fb158 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sun, 10 May 2026 12:44:15 +0100 Subject: [PATCH 065/291] Backport PR #31647: FIX: Pin rstcheck to prevent CI failure --- .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 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 066/291] 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 203af572e8161399b1cc66cbccae7eb9b07d3182 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 067/291] Backport PR #31649: DOC: Prevent ticks from being cut off in tick rotation example --- 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 068/291] 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 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 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 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 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 782a1f4d7b5ec33643aa7e9c44baf70a71106ea3 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sun, 10 May 2026 23:23:21 -0400 Subject: [PATCH 069/291] Backport PR #30108: 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 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 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 zcmcgw2{@G7|35PZqsa2VA=xF}grO{BuPjM%wa6ADOUTwxc4lr;uAL~^Bf3ep3fZRA zwG}Bslu;Oan8;ZF=bh@_dVcHw`91%i=kXZtocFxv{eC~&Ig!7Z7;i{LJsin8e)V|?YX(4{`(o+|nuyILUB>Q)w4aQ?LqSfWE3ySzv=fBTO zyxlTCuQl*l^JS_zUZ}cdwklvEJ!q=KxxDu$0A5!5OnL!83gyrRBu9)m&{dLIR*g2u zZJX{NyQtUIrCHS`JVq}iRnWb*nOUGP;D7-@gKbg(10+Wp>N^Ih!aXGXEgGKw*MM?LolneRH0W~yD~Q`A`H_wn7CimvXdoRQ{p!gHn1 znm^P(*xlBga*iQf7l{v8={|3yBx5*GmmO5~*fs3FeU(kht5>h)8tRu?iF18^#(H53 zK{~Q#tBgzQ#F<9_wG}5u2X~ldrgn%`{;RQ~X@~n(9_#h%Gp6;{({HPn=2ur3`75KB z){pW>>8Bpr?sXCsP24iqR@EV`oej9g#R8Zm9PJG54ndciZMK1i(2BVc8CA?(beV`Nt0h+{sD)OYqx zoBg$MmYw0EN$AxKXga2ordEFvijY6?6cBDWblE0GpX?$QNn zbBVx>v(P;<2ET49q1)!11UqQJVu|YI?gIB<{nCKp^ZPKugl$B$$REpe!?@Woi^N&YskKq zh9<3HDYbO8R75tmCiUqlv+YUEPrJ?6!DR!m-ID8`ba06BnmoHW&`Vm7jITJGi~~HW zpyt;?xcplf!pkmQFX7ctm9)b$EFg9-*d9x1*=oq5tBszy4hze35!8@JX{=UU7?LoC zDyJ7iIfy~)2BT+EkZ(s~vJG?1PWy?j?ra>*5}8j@%f$s9}BRCQ1gxXcMt_FENq_m#qzKG?|EZm>lEOJDm$=1JhKT`+{83LA7OJDFr6sK&bG|+9 z!-tcJEb5k{&idDgc&(Jn__Z#5s?l>azLpc%myTIFZIU=A881XW2QLW%!Qb|>QcZ@i z(L%p!@7n}~-l0WX3RrviJWcaWxyR_Q@DWNfD0+e#TNS#}!MO6UISY!=!zRMSs1zDS zsJxDaNU_k@R`>;rrp)a5A0{yG^ONUa;pF35v#y{3@24eg(#gm`a+9Yt?tNnu*PztU z+FE_Rok^zMYNnsv`t68!rnbv_q8Y)qqFRBS4Sra#>Y@aDPZ&~-^&H+gxa2CB)I9Y5 zjMo)T-93jrP#?>s`}1?WIJy_BHKE{j>=7Y#DDcew(E%tD;mq|Q^&LHOjmemZI_co9 z0v}&RKo#7_N{E0cDP}{1RjdF>!%V8=H(~NV91LWyksLt!`4P0VJAe$*#lC@7?W|3VN@m5 zHE|~LC$SVQgf62PLRput^G$EjgF}oS+{= zIf*rhqR3>074$QTxbLpeuiDl2tocFpBQ6JPE5wOv?QDG6l^(4vmczXkvu%ScNhCu+ zP!S_#Ks^T~Tq*UCeBFa3cHKSb5-GD2EW16CiVKqfPfV1q`p`~$kj+|q7u@Zhn5rCi zT*kU{yh`Ns;j=nr!bfDCnVh_HsNhvpX;@jvHh6RG*-ym^w?&epU;& zSClad;BC6J9Nri0 zBH~pD8(|3rV9-S#Kq7kZkpYhLFdN|$vsDxcWf4+>R7n|>YTY54KL#2nQFO&XsbmIQ(F_H5Kaw-b5mMUywhSglMB1X zlS^%jV!JQdDu{hgq|FF)(WN5Y*K0!QSgrmkVe8@Bk%# z{g^dn8ByvjH#adkt_6L?U+3ov(&t{jk$jc3%6c`xW4E-K!U@+=46*5H0=7pY(g>14 zFbfoWz3m_=H1IO?AOzz3JF$15UJh*o#e3nH1T5tYQZ79S;ML?N4qm9E{5QhtW9N;; zb31O8Ys9U%a&+iu`M_Ecjl!>n(W&M#n=-S*Z5(oN0q^NxH8VI zvx8UvE#HEkJi>CC_T;!kO(U!|L zpw0y;R@`*w;2u~@E@^z1xaDy=rp{iQ?Z7L*-$=6w4tR?QM<0uN0o*b5S zm^dB)c#Mh#$o&SLU6j!*>HfK8*4MK3GKk5^Cj#;7kJt%ASZJ%3&hzl7^QyO#7d+eyX?OQJ^$I06JbyGm|}%`r_c;?OyQA+mr3^&zUjNz(#R{xqzs&8b>kPIhhjK-;uh#H73vX1P@ENyBoGyZCk-37ehVpTcV3_> z9h6HD`J;Lt!9cC-^Yt6lVnJn}aYf@AAm%CO@-|R1ns|3$P$WO2EeMq$!66J+Kne{(YoaUjx|+XxywDb^3k0yTqdWH^LOHp?G4z zAyKmB36_EUKL&;YhU8*{A+p?s1d!w*RZtOKbb`^~h5=zXCU?N2yw_I=5O`?dJY)7) z$2!?385c78V?5A1$@K}k7f%wHJOD|8sMs!a$$n%?s}~c?UnN$m;>P8rO!7N5^~|rJ z{Z3}r6RZ<@`0`m(Af?<%jNCHdJn?4JS-g9Mlj*CF1Du3 z?>=6rDN@)zMox{I8U3PN2Jbjda}-xK|?|UC)ENb1SFs`KJfs-yAYh8AZ)!hOJPt1 z|LYt~F=0k1O39wh%z^q*ln$59WY7{3_6ENBrlMXDEzC4X>Msz8hzQ>BfTc0M z&W%W4M@9H0m-|+=O0Orbwd;U@+65zVxu~TviQ_T4S)y~H(`%D-B9S(o92QC&W-Qc~ z_f97F(dYpIpGG@^wABHp;jOQlmuo=vRQBE>CQ)IrtHn{NYB5kVmXXd9f8v@PH3YiI z?kjSGLSa`g0gU){n1G=dG|^Z$Nn=prw_l(vst#5%V}lIL)plII$uno&Rnkn@fbjn7 z$og|C!UZHbz{8ZCh;0Df^J8e-k{e+NnnU(hSEkTI39CFf4k;)O;tww&^vg!7sD$!2 z`XZSnQsDDtT_CQGT4X~aL=%Q;jYLXGKGH{Vm^)lBROKE7f4n4Bf-(<)W8cMNVdRtn zUXNfGUz8=SZiU1-?N`_9e()2ICerZnt%eHqs@e+`iWVoICrvy{&=%Y1ukj_YenzsP zOAMIQ{RG!3&X4NJVRBP^22Z+l&$hV2;dGUa=lb$|E1mCYM%e0S;@ax6^o;>Vg@Dui z@@l5QVi-+9T_bF2;(5T&3InsWn^%yGGKFLm$%k$x(>m-W7*YiY^Zg)*ENqq%XOAv%9j%cDPv*6x#@(IW1By;zCQEfo_+@%+JH6wr z&)gfR#o&qaMnNFH1q%AtS@MQepShc5d5QO6KD22KmVqq(13uvO~hNJBqp$n6iRblI)RX&X$GfxuT zFV?73Xg%;;=uCSkv^+u&DEREQPfw7&o0u+;Bpt6r@@ zN`Y^&{}k)@eQXNdM3pFl5xmE;$UH<_Q}+OTpX?K$y{)aqSG`uSy!TG~C3b9c>E_Dc zJDfirdAr~C_l5j{BWE!3S6gAiR}Zow_>Tt{*DpF{d_+nSTU7fc>HtdVW;Ia(GWc>< zlo;T#v8TWf__mjB7UYxtm(cnTU;Qu8|9M*V&)YKMxE=vLq&LG$C9Q3GW46=Z_Vush zF7v|US=`(_i4L;3(ajW?l$A0}6#j5mnV=7%_7UA#J>Bm0Rv-Huf8uuSSVgB0xplY< z#^p9q0l`f5feD3BOIkI&HP7J;G*afi%iEnbwh^K17rxo{p%=zPz*`@8)`%znnp*## z82WpK#P`=OUHx6iXdQhgUHDKni6+pyVI^#KuB3K8p|w`kG0 z#y%9uLDpMS9*z2Xa7%x~Ut;syzb|IzkSUT+MByFBJ;ppk2GmV?EO zm5@pE6;o5w)~j}*mfB0Bxrs3jXQuH~nqbQD`0lP{;crDRE&ju9A`GQ5PrE!*zKD;I zZW%B|8v9;o&_5Tc?p7r50zEU6O zqSNZ3_FyFREu6(kc<9``wodCAL{)jx-2k^&8 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 11af330ba17c359ad998a5421d8e9ba27404d52e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 11 May 2026 14:46:53 -0400 Subject: [PATCH 070/291] Backport PR #31578: FIX: URL links in SVG should have 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 5310f9754b5323421da262e882b312e11699f00a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 11 May 2026 19:12:35 -0400 Subject: [PATCH 071/291] 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 38a587c152965c1ce33272af32244ed5a938418b Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Tue, 12 May 2026 11:17:54 +0100 Subject: [PATCH 072/291] Backport PR #31659: ci: Re-arrange AppVeyor 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 75ea50cc0893a1669fe257e01842de1e1c8fbb1b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 12 May 2026 16:10:52 -0400 Subject: [PATCH 073/291] DOC: Prepare GitHub stats for 3.11 rc2 --- doc/release/github_stats.rst | 75 +++++++++++++++++++++++++++++++---- doc/release/release_notes.rst | 9 ++++- 2 files changed, 76 insertions(+), 8 deletions(-) diff --git a/doc/release/github_stats.rst b/doc/release/github_stats.rst index 62c3242b7eb7..d6beb7b6b059 100644 --- a/doc/release/github_stats.rst +++ b/doc/release/github_stats.rst @@ -2,17 +2,17 @@ .. _github-stats: -GitHub statistics for 3.11.0 (Apr 24, 2026) +GitHub statistics for 3.11.0 (May 12, 2026) =========================================== -GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2026/04/24 +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2026/05/12 These lists are automatically generated, and may be incomplete or contain duplicates. -We closed 246 issues and merged 764 pull requests. +We closed 257 issues and merged 812 pull requests. The full list can be seen `on GitHub `__ -The following 264 authors contributed 4590 commits. +The following 266 authors contributed 4674 commits. * 34j * Aaratrika-Shelly @@ -33,8 +33,8 @@ The following 264 authors contributed 4590 commits. * Alexandra Khoo * Allison * alphanoobie -* Aman Kushwaha * AMAN KUSHWAHA +* Aman Kushwaha * Aman Nijjar * Aman Parganiha * Aman Srivastava @@ -58,6 +58,7 @@ The following 264 authors contributed 4590 commits. * Ben Root * Bodhi Silberling * Brian Christian +* Brian Lau * BriAnna Foreman * brk * Carlos Ramos Carreño @@ -234,6 +235,7 @@ The following 264 authors contributed 4590 commits. * Raphael Erik Hviding * Raphael Quast * RETHICK CB +* Ricardo Peres * RogueRebel33 * Roman * Roman A @@ -281,8 +283,56 @@ The following 264 authors contributed 4590 commits. GitHub issues and pull requests: -Pull Requests (764): +Pull Requests (812): +* :ghpull:`31662`: Backport PR #31659 on branch v3.11.x (ci: Re-arrange AppVeyor pipeline) +* :ghpull:`31659`: ci: Re-arrange AppVeyor pipeline +* :ghpull:`31658`: Backport PR #31578 on branch v3.11.x (FIX: URL links in SVG should have target='_blank') +* :ghpull:`31578`: FIX: URL links in SVG should have target='_blank' +* :ghpull:`31654`: Backport PR #30108 on branch v3.11.x (Fix constrained layout applying pad multiple times) +* :ghpull:`30108`: Fix constrained layout applying pad multiple times +* :ghpull:`31651`: Backport PR #31649 on branch v3.11.x (DOC: Prevent ticks from being cut off in tick rotation example) +* :ghpull:`31650`: Backport PR #31647 on branch v3.11.x (FIX: Pin rstcheck to prevent CI failure) +* :ghpull:`31649`: DOC: Prevent ticks from being cut off in tick rotation example +* :ghpull:`31647`: FIX: Pin rstcheck to prevent CI failure +* :ghpull:`31646`: Backport PR #31632 on branch v3.11.x (FIX: Prohibit special TeX chars in pgf metadata) +* :ghpull:`31632`: FIX: Prohibit special TeX chars in pgf metadata +* :ghpull:`31643`: Backport PR #31609 on branch v3.11.x (DOC: Improve autoscaling and margin docs) +* :ghpull:`31644`: Backport PR #31579 on branch v3.11.x (DOC: Document that bar() errorbars do not support individual coloring) +* :ghpull:`31579`: DOC: Document that bar() errorbars do not support individual coloring +* :ghpull:`31609`: DOC: Improve autoscaling and margin docs +* :ghpull:`31640`: Backport PR #31638 on branch v3.11.x (Bump the actions group with 2 updates) +* :ghpull:`31639`: Backport PR #31628 on branch v3.11.x (FIX: use axis lines tight bbox within axis artist tight bbox) +* :ghpull:`31638`: Bump the actions group with 2 updates +* :ghpull:`31637`: Backport PR #31634 on branch v3.11.x (Fix some font-related issues) +* :ghpull:`31628`: FIX: use axis lines tight bbox within axis artist tight bbox +* :ghpull:`31634`: Fix some font-related issues +* :ghpull:`31636`: Backport PR #31630 on branch v3.11.x (Restore PolarTransform(apply_theta_transforms) parameter) +* :ghpull:`31630`: Restore PolarTransform(apply_theta_transforms) parameter +* :ghpull:`31631`: Backport PR #31557 on branch v3.11.x (FIX: Added ft2font null checks added) +* :ghpull:`31629`: Backport PR #31621 on branch v3.11.x (Make Scale axis parameter handling more flexible) +* :ghpull:`31557`: FIX: Added ft2font null checks added +* :ghpull:`31621`: Make Scale axis parameter handling more flexible +* :ghpull:`31627`: Backport PR #31625 on branch v3.11.x (DOC: Inline ScalarMappable reStructuredText entries) +* :ghpull:`31626`: Backport PR #25478 on branch v3.11.x ([BUG] Fix alpha bug on 3D PathCollection plots.) +* :ghpull:`31625`: DOC: Inline ScalarMappable reStructuredText entries +* :ghpull:`25478`: [BUG] Fix alpha bug on 3D PathCollection plots. +* :ghpull:`31611`: Backport PR #31608 on branch v3.11.x (Remove outdated comment re: implementation of hinting_factor.) +* :ghpull:`31608`: Remove outdated comment re: implementation of hinting_factor. +* :ghpull:`31602`: Backport PR #31599 on branch v3.11.x (Bump the actions group with 2 updates) +* :ghpull:`31603`: Backport PR #31594 on branch v3.11.x (DOC: Explain how to selectively restore ticks that are removed by sharex) +* :ghpull:`31594`: DOC: Explain how to selectively restore ticks that are removed by sharex +* :ghpull:`31601`: Backport PR #31600 on branch v3.11.x (Bump https://github.com/astral-sh/ruff-pre-commit from v0.15.11 to 0.15.12) +* :ghpull:`31599`: Bump the actions group with 2 updates +* :ghpull:`31600`: Bump https://github.com/astral-sh/ruff-pre-commit from v0.15.11 to 0.15.12 +* :ghpull:`31592`: Backport PR #31588 on branch v3.11.x (Expire some missed deprecations from 3.9) +* :ghpull:`31588`: Expire some missed deprecations from 3.9 +* :ghpull:`31583`: Backport PR #31577 on branch v3.11.x (FIX: Polar Radial Tick Warnings Labels Bug) +* :ghpull:`31577`: FIX: Polar Radial Tick Warnings Labels Bug +* :ghpull:`31582`: Backport PR #31580 on branch v3.11.x (DOC: added unregister to colormap guide) +* :ghpull:`31580`: DOC: added unregister to colormap guide +* :ghpull:`31564`: Backport PR #31563 on branch v3.11.x (LIC: remove carlogo license) +* :ghpull:`31563`: LIC: remove carlogo license * :ghpull:`31561`: Fixed bug with an uninitialized colormap in parallel threads * :ghpull:`31555`: FIX: removing colorbar's axes also removes colorbar * :ghpull:`31560`: merge up v3.10.9 @@ -1048,8 +1098,19 @@ Pull Requests (764): * :ghpull:`29079`: DOC: Replaced colormap for colorblindness * :ghpull:`29077`: DOC: Replaced green with blue for colorblindness -Issues (246): +Issues (257): +* :ghissue:`23290`: [Bug]: Constrained Layout scaling of layouts with submerged spines +* :ghissue:`31622`: [Bug]: ``tight`` and ``constrained`` layouts honouring invisible parts of ``floating_axis`` +* :ghissue:`31624`: [MNT]: PolarTransform deprecation didn't warn +* :ghissue:`31590`: Should ``_make_axis_parameter_optional`` handle ``None``? +* :ghissue:`25446`: [Bug]: Nan values in scatter 3d plot show in black colour when alpha parameter is passed. +* :ghissue:`22546`: [Doc]: svg.fonttype: None in custom style sheet gives an error +* :ghissue:`24958`: [Doc]: Provide a working example for turning on specific axes labels when sharex or sharey are used with subplots +* :ghissue:`25818`: [Doc]: Heatmap border pixels leak outside grid +* :ghissue:`31574`: [Bug]: polar projection with ``labels`` on ``set_ticks`` gives UserWarning +* :ghissue:`14480`: Multicolor errorbars cannot have caps +* :ghissue:`31330`: [Bug]: Crash when removing colorbar axes in a constrained layout * :ghissue:`14235`: Add \underline to mathtext? * :ghissue:`31462`: [Bug]: Errorbar plot on log-scaled Axes sets incorrect automatic lower limits * :ghissue:`30859`: [Bug]: ax.relim() ignores scatter artist diff --git a/doc/release/release_notes.rst b/doc/release/release_notes.rst index e2cd258ee3a7..d652f5dbcf0f 100644 --- a/doc/release/release_notes.rst +++ b/doc/release/release_notes.rst @@ -13,6 +13,13 @@ Release notes .. include:: release_notes_next.rst +Version 3.11 +^^^^^^^^^^^^ +.. toctree:: + :maxdepth: 1 + + github_stats.rst + Version 3.10 ^^^^^^^^^^^^ .. toctree:: @@ -23,7 +30,7 @@ Version 3.10 ../api/prev_api_changes/api_changes_3.10.7.rst ../api/prev_api_changes/api_changes_3.10.1.rst ../api/prev_api_changes/api_changes_3.10.0.rst - github_stats.rst + prev_whats_new/github_stats_3.10.9.rst prev_whats_new/github_stats_3.10.8.rst prev_whats_new/github_stats_3.10.7.rst prev_whats_new/github_stats_3.10.6.rst From bcdae6fc54232ef2b99a7bb74e7c478dbf84c364 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 12 May 2026 16:14:27 -0400 Subject: [PATCH 074/291] REL: v3.11.0rc2 This is the second release candidate for the meso release 3.11.0. From 7c4bea54917c1bf3e37316c30cedf48a522c14ef Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 12 May 2026 16:15:51 -0400 Subject: [PATCH 075/291] BLD: bump branch away from tag So the tarballs from GitHub are stable. From d053d9596936ae21d5bff8fabb15d22ef3a0c53a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 11 May 2026 11:28:01 -0400 Subject: [PATCH 076/291] 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 077/291] 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 078/291] 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 079/291] 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 080/291] 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 081/291] 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 082/291] 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 083/291] 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 084/291] 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 085/291] 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 086/291] 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. + //