From 146deee4734cdcd33dfc9e932f9b232c9a588109 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 13 Feb 2024 13:28:13 +0100 Subject: [PATCH 1/2] Deprecate positional use of most arguments of plotting functions This increases maintainability for developers and disallows bad practice of passing lots of positional arguments for users. If in doubt, I've erred on the side of allowing as much positional arguments as possible as long as the intent of a call is still readable. Note: This was originally motivated by bxp() and boxplot() having many overlapping parameters but differently ordered. While at it, I think it's better to rollout the change to all plotting functions and communicate that in one go rather than having lots of individual change notices over time. Also, the freedom to reorder parameters only sets in after the deprecation. So let's start this now. --- .../deprecations/27786-TH.rst | 7 ++++ .../examples/lines_bars_and_markers/cohere.py | 2 +- .../lines_bars_and_markers/csd_demo.py | 2 +- .../lines_bars_and_markers/psd_demo.py | 2 +- galleries/examples/statistics/boxplot_demo.py | 10 ++--- lib/matplotlib/axes/_axes.py | 22 ++++++++++ lib/matplotlib/axes/_axes.pyi | 41 ++++++++++--------- 7 files changed, 59 insertions(+), 27 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/27786-TH.rst diff --git a/doc/api/next_api_changes/deprecations/27786-TH.rst b/doc/api/next_api_changes/deprecations/27786-TH.rst new file mode 100644 index 000000000000..6b66e0dba963 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/27786-TH.rst @@ -0,0 +1,7 @@ +Positional parameters in plotting functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Many plotting functions will restrict positional arguments to the first few parameters +in the future. All further configuration parameters will have to be passed as keyword +arguments. This is to enforce better code and and allow for future changes with reduced +risk of breaking existing code. diff --git a/galleries/examples/lines_bars_and_markers/cohere.py b/galleries/examples/lines_bars_and_markers/cohere.py index 64124e37645e..917188292311 100644 --- a/galleries/examples/lines_bars_and_markers/cohere.py +++ b/galleries/examples/lines_bars_and_markers/cohere.py @@ -27,7 +27,7 @@ axs[0].set_ylabel('s1 and s2') axs[0].grid(True) -cxy, f = axs[1].cohere(s1, s2, 256, 1. / dt) +cxy, f = axs[1].cohere(s1, s2, NFFT=256, Fs=1. / dt) axs[1].set_ylabel('Coherence') plt.show() diff --git a/galleries/examples/lines_bars_and_markers/csd_demo.py b/galleries/examples/lines_bars_and_markers/csd_demo.py index b2d903ae0885..6d7a9746e88e 100644 --- a/galleries/examples/lines_bars_and_markers/csd_demo.py +++ b/galleries/examples/lines_bars_and_markers/csd_demo.py @@ -34,7 +34,7 @@ ax1.set_ylabel('s1 and s2') ax1.grid(True) -cxy, f = ax2.csd(s1, s2, 256, 1. / dt) +cxy, f = ax2.csd(s1, s2, NFFT=256, Fs=1. / dt) ax2.set_ylabel('CSD (dB)') plt.show() diff --git a/galleries/examples/lines_bars_and_markers/psd_demo.py b/galleries/examples/lines_bars_and_markers/psd_demo.py index 52587fd6d7bf..fa0a8565b6ff 100644 --- a/galleries/examples/lines_bars_and_markers/psd_demo.py +++ b/galleries/examples/lines_bars_and_markers/psd_demo.py @@ -30,7 +30,7 @@ ax0.plot(t, s) ax0.set_xlabel('Time (s)') ax0.set_ylabel('Signal') -ax1.psd(s, 512, 1 / dt) +ax1.psd(s, NFFT=512, Fs=1 / dt) plt.show() diff --git a/galleries/examples/statistics/boxplot_demo.py b/galleries/examples/statistics/boxplot_demo.py index eca0e152078e..f7f1078b2d27 100644 --- a/galleries/examples/statistics/boxplot_demo.py +++ b/galleries/examples/statistics/boxplot_demo.py @@ -34,23 +34,23 @@ axs[0, 0].set_title('basic plot') # notched plot -axs[0, 1].boxplot(data, 1) +axs[0, 1].boxplot(data, notch=True) axs[0, 1].set_title('notched plot') # change outlier point symbols -axs[0, 2].boxplot(data, 0, 'gD') +axs[0, 2].boxplot(data, sym='gD') axs[0, 2].set_title('change outlier\npoint symbols') # don't show outlier points -axs[1, 0].boxplot(data, 0, '') +axs[1, 0].boxplot(data, sym='') axs[1, 0].set_title("don't show\noutlier points") # horizontal boxes -axs[1, 1].boxplot(data, 0, 'rs', 0) +axs[1, 1].boxplot(data, sym='rs', vert=False) axs[1, 1].set_title('horizontal boxes') # change whisker length -axs[1, 2].boxplot(data, 0, 'rs', 0, 0.75) +axs[1, 2].boxplot(data, sym='rs', vert=False, whis=0.75) axs[1, 2].set_title('change whisker length') fig.subplots_adjust(left=0.08, right=0.98, bottom=0.05, top=0.9, diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index b65004b8c272..7a022104cfa1 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -1100,6 +1100,7 @@ def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs): self._request_autoscale_view("x") return p + @_api.make_keyword_only("3.9", "label") @_preprocess_data(replace_names=["y", "xmin", "xmax", "colors"], label_namer="y") def hlines(self, y, xmin, xmax, colors=None, linestyles='solid', @@ -1191,6 +1192,7 @@ def hlines(self, y, xmin, xmax, colors=None, linestyles='solid', self._request_autoscale_view() return lines + @_api.make_keyword_only("3.9", "label") @_preprocess_data(replace_names=["x", "ymin", "ymax", "colors"], label_namer="x") def vlines(self, x, ymin, ymax, colors=None, linestyles='solid', @@ -1282,6 +1284,7 @@ def vlines(self, x, ymin, ymax, colors=None, linestyles='solid', self._request_autoscale_view() return lines + @_api.make_keyword_only("3.9", "orientation") @_preprocess_data(replace_names=["positions", "lineoffsets", "linelengths", "linewidths", "colors", "linestyles"]) @@ -2088,6 +2091,7 @@ def acorr(self, x, **kwargs): """ return self.xcorr(x, x, **kwargs) + @_api.make_keyword_only("3.9", "normed") @_preprocess_data(replace_names=["x", "y"], label_namer="y") def xcorr(self, x, y, normed=True, detrend=mlab.detrend_none, usevlines=True, maxlags=10, **kwargs): @@ -3155,6 +3159,7 @@ 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.9", "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, @@ -3434,6 +3439,7 @@ def _errorevery_to_mask(x, errorevery): everymask[errorevery] = True return everymask + @_api.make_keyword_only("3.9", "ecolor") @_preprocess_data(replace_names=["x", "y", "xerr", "yerr"], label_namer="y") @_docstring.dedent_interpd @@ -3810,6 +3816,7 @@ def apply_mask(arrays, mask): return errorbar_container # (l0, caplines, barcols) + @_api.make_keyword_only("3.9", "notch") @_preprocess_data() @_api.rename_parameter("3.9", "labels", "tick_labels") def boxplot(self, x, notch=None, sym=None, vert=None, whis=None, @@ -4144,6 +4151,7 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None, capwidths=capwidths, label=label) return artists + @_api.make_keyword_only("3.9", "widths") def bxp(self, bxpstats, positions=None, widths=None, vert=True, patch_artist=False, shownotches=False, showmeans=False, showcaps=True, showbox=True, showfliers=True, @@ -4636,6 +4644,7 @@ def invalid_shape_exception(csize, xsize): colors = None # use cmap, norm after collection is created return c, colors, edgecolors + @_api.make_keyword_only("3.9", "marker") @_preprocess_data(replace_names=["x", "y", "s", "linewidths", "edgecolors", "c", "facecolor", "facecolors", "color"], @@ -4916,6 +4925,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, return collection + @_api.make_keyword_only("3.9", "gridsize") @_preprocess_data(replace_names=["x", "y", "C"], label_namer="y") @_docstring.dedent_interpd def hexbin(self, x, y, C=None, gridsize=100, bins=None, @@ -6698,6 +6708,7 @@ def clabel(self, CS, levels=None, **kwargs): #### Data analysis + @_api.make_keyword_only("3.9", "range") @_preprocess_data(replace_names=["x", 'weights'], label_namer="x") def hist(self, x, bins=None, range=None, density=False, weights=None, cumulative=False, bottom=None, histtype='bar', align='mid', @@ -7245,6 +7256,7 @@ def stairs(self, values, edges=None, *, self._request_autoscale_view() return patch + @_api.make_keyword_only("3.9", "range") @_preprocess_data(replace_names=["x", "y", "weights"]) @_docstring.dedent_interpd def hist2d(self, x, y, bins=10, range=None, density=False, weights=None, @@ -7454,6 +7466,7 @@ def ecdf(self, x, weights=None, *, complementary=False, line.sticky_edges.x[:] = [0, 1] return line + @_api.make_keyword_only("3.9", "NFFT") @_preprocess_data(replace_names=["x"]) @_docstring.dedent_interpd def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, @@ -7565,6 +7578,7 @@ def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, else: return pxx, freqs, line + @_api.make_keyword_only("3.9", "NFFT") @_preprocess_data(replace_names=["x", "y"], label_namer="y") @_docstring.dedent_interpd def csd(self, x, y, NFFT=None, Fs=None, Fc=None, detrend=None, @@ -7667,6 +7681,7 @@ def csd(self, x, y, NFFT=None, Fs=None, Fc=None, detrend=None, else: return pxy, freqs, line + @_api.make_keyword_only("3.9", "Fs") @_preprocess_data(replace_names=["x"]) @_docstring.dedent_interpd def magnitude_spectrum(self, x, Fs=None, Fc=None, window=None, @@ -7753,6 +7768,7 @@ def magnitude_spectrum(self, x, Fs=None, Fc=None, window=None, return spec, freqs, line + @_api.make_keyword_only("3.9", "Fs") @_preprocess_data(replace_names=["x"]) @_docstring.dedent_interpd def angle_spectrum(self, x, Fs=None, Fc=None, window=None, @@ -7822,6 +7838,7 @@ def angle_spectrum(self, x, Fs=None, Fc=None, window=None, return spec, freqs, lines[0] + @_api.make_keyword_only("3.9", "Fs") @_preprocess_data(replace_names=["x"]) @_docstring.dedent_interpd def phase_spectrum(self, x, Fs=None, Fc=None, window=None, @@ -7891,6 +7908,7 @@ def phase_spectrum(self, x, Fs=None, Fc=None, window=None, return spec, freqs, lines[0] + @_api.make_keyword_only("3.9", "NFFT") @_preprocess_data(replace_names=["x", "y"]) @_docstring.dedent_interpd def cohere(self, x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, @@ -7955,6 +7973,7 @@ def cohere(self, x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, return cxy, freqs + @_api.make_keyword_only("3.9", "NFFT") @_preprocess_data(replace_names=["x"]) @_docstring.dedent_interpd def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, @@ -8111,6 +8130,7 @@ def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, return spec, freqs, t, im + @_api.make_keyword_only("3.9", "precision") @_docstring.dedent_interpd def spy(self, Z, precision=0, marker=None, markersize=None, aspect='equal', origin="upper", **kwargs): @@ -8301,6 +8321,7 @@ def matshow(self, Z, **kwargs): mticker.MaxNLocator(nbins=9, steps=[1, 2, 5, 10], integer=True)) return im + @_api.make_keyword_only("3.9", "vert") @_preprocess_data(replace_names=["dataset"]) def violinplot(self, dataset, positions=None, vert=True, widths=0.5, showmeans=False, showextrema=True, showmedians=False, @@ -8412,6 +8433,7 @@ def _kde_method(X, coords): widths=widths, showmeans=showmeans, showextrema=showextrema, showmedians=showmedians, side=side) + @_api.make_keyword_only("3.9", "vert") def violin(self, vpstats, positions=None, vert=True, widths=0.5, showmeans=False, showextrema=True, showmedians=False, side='both'): """ diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index b70d330aa442..be0a0e48d662 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -166,8 +166,8 @@ class Axes(_AxesBase): xmax: float | ArrayLike, colors: ColorType | Sequence[ColorType] | None = ..., linestyles: LineStyleType = ..., - label: str = ..., *, + label: str = ..., data=..., **kwargs ) -> LineCollection: ... @@ -178,14 +178,15 @@ class Axes(_AxesBase): ymax: float | ArrayLike, colors: ColorType | Sequence[ColorType] | None = ..., linestyles: LineStyleType = ..., - label: str = ..., *, + label: str = ..., data=..., **kwargs ) -> LineCollection: ... def eventplot( self, positions: ArrayLike | Sequence[ArrayLike], + *, orientation: Literal["horizontal", "vertical"] = ..., lineoffsets: float | Sequence[float] = ..., linelengths: float | Sequence[float] = ..., @@ -193,7 +194,6 @@ class Axes(_AxesBase): colors: ColorType | Sequence[ColorType] | None = ..., alpha: float | Sequence[float] | None = ..., linestyles: LineStyleType | Sequence[LineStyleType] = ..., - *, data=..., **kwargs ) -> EventCollection: ... @@ -227,11 +227,11 @@ class Axes(_AxesBase): self, x: ArrayLike, y: ArrayLike, + *, normed: bool = ..., detrend: Callable[[ArrayLike], ArrayLike] = ..., usevlines: bool = ..., maxlags: int = ..., - *, data = ..., **kwargs ) -> tuple[np.ndarray, np.ndarray, LineCollection | Line2D, Line2D | None]: ... @@ -300,6 +300,7 @@ class Axes(_AxesBase): def pie( self, x: ArrayLike, + *, explode: ArrayLike | None = ..., labels: Sequence[str] | None = ..., colors: ColorType | Sequence[ColorType] | None = ..., @@ -315,7 +316,6 @@ class Axes(_AxesBase): center: tuple[float, float] = ..., frame: bool = ..., rotatelabels: bool = ..., - *, normalize: bool = ..., hatch: str | Sequence[str] | None = ..., data=..., @@ -329,6 +329,7 @@ class Axes(_AxesBase): yerr: float | ArrayLike | None = ..., xerr: float | ArrayLike | None = ..., fmt: str = ..., + *, ecolor: ColorType | None = ..., elinewidth: float | None = ..., capsize: float | None = ..., @@ -339,13 +340,13 @@ class Axes(_AxesBase): xuplims: bool | ArrayLike = ..., errorevery: int | tuple[int, int] = ..., capthick: float | None = ..., - *, data=..., **kwargs ) -> ErrorbarContainer: ... def boxplot( self, x: ArrayLike | Sequence[ArrayLike], + *, notch: bool | None = ..., sym: str | None = ..., vert: bool | None = ..., @@ -373,13 +374,13 @@ class Axes(_AxesBase): zorder: float | None = ..., capwidths: float | ArrayLike | None = ..., label: Sequence[str] | None = ..., - *, data=..., ) -> dict[str, Any]: ... def bxp( self, bxpstats: Sequence[dict[str, Any]], positions: ArrayLike | None = ..., + *, widths: float | ArrayLike | None = ..., vert: bool = ..., patch_artist: bool = ..., @@ -406,6 +407,7 @@ class Axes(_AxesBase): y: float | ArrayLike, s: float | ArrayLike | None = ..., c: ArrayLike | Sequence[ColorType] | ColorType | None = ..., + *, marker: MarkerType | None = ..., cmap: str | Colormap | None = ..., norm: str | Normalize | None = ..., @@ -413,7 +415,6 @@ class Axes(_AxesBase): vmax: float | None = ..., alpha: float | None = ..., linewidths: float | Sequence[float] | None = ..., - *, edgecolors: Literal["face", "none"] | ColorType | Sequence[ColorType] | None = ..., plotnonfinite: bool = ..., data=..., @@ -424,6 +425,7 @@ class Axes(_AxesBase): x: ArrayLike, y: ArrayLike, C: ArrayLike | None = ..., + *, gridsize: int | tuple[int, int] = ..., bins: Literal["log"] | int | Sequence[float] | None = ..., xscale: Literal["linear", "log"] = ..., @@ -439,7 +441,6 @@ class Axes(_AxesBase): reduce_C_function: Callable[[np.ndarray | list[float]], float] = ..., mincnt: int | None = ..., marginals: bool = ..., - *, data=..., **kwargs ) -> PolyCollection: ... @@ -542,6 +543,7 @@ class Axes(_AxesBase): self, x: ArrayLike | Sequence[ArrayLike], bins: int | Sequence[float] | str | None = ..., + *, range: tuple[float, float] | None = ..., density: bool = ..., weights: ArrayLike | None = ..., @@ -555,7 +557,6 @@ class Axes(_AxesBase): color: ColorType | Sequence[ColorType] | None = ..., label: str | Sequence[str] | None = ..., stacked: bool = ..., - *, data=..., **kwargs ) -> tuple[ @@ -583,12 +584,12 @@ class Axes(_AxesBase): | tuple[int, int] | ArrayLike | tuple[ArrayLike, ArrayLike] = ..., + *, range: ArrayLike | None = ..., density: bool = ..., weights: ArrayLike | None = ..., cmin: float | None = ..., cmax: float | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray, np.ndarray, QuadMesh]: ... @@ -606,6 +607,7 @@ class Axes(_AxesBase): def psd( self, x: ArrayLike, + *, NFFT: int | None = ..., Fs: float | None = ..., Fc: int | None = ..., @@ -618,7 +620,6 @@ class Axes(_AxesBase): sides: Literal["default", "onesided", "twosided"] | None = ..., scale_by_freq: bool | None = ..., return_line: bool | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, Line2D]: ... @@ -626,6 +627,7 @@ class Axes(_AxesBase): self, x: ArrayLike, y: ArrayLike, + *, NFFT: int | None = ..., Fs: float | None = ..., Fc: int | None = ..., @@ -638,44 +640,43 @@ class Axes(_AxesBase): sides: Literal["default", "onesided", "twosided"] | None = ..., scale_by_freq: bool | None = ..., return_line: bool | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray] | tuple[np.ndarray, np.ndarray, Line2D]: ... def magnitude_spectrum( self, x: ArrayLike, + *, Fs: float | None = ..., Fc: int | None = ..., window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ..., pad_to: int | None = ..., sides: Literal["default", "onesided", "twosided"] | None = ..., scale: Literal["default", "linear", "dB"] | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray, Line2D]: ... def angle_spectrum( self, x: ArrayLike, + *, Fs: float | None = ..., Fc: int | None = ..., window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ..., pad_to: int | None = ..., sides: Literal["default", "onesided", "twosided"] | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray, Line2D]: ... def phase_spectrum( self, x: ArrayLike, + *, Fs: float | None = ..., Fc: int | None = ..., window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = ..., pad_to: int | None = ..., sides: Literal["default", "onesided", "twosided"] | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray, Line2D]: ... @@ -683,6 +684,7 @@ class Axes(_AxesBase): self, x: ArrayLike, y: ArrayLike, + *, NFFT: int = ..., Fs: float = ..., Fc: int = ..., @@ -693,13 +695,13 @@ class Axes(_AxesBase): pad_to: int | None = ..., sides: Literal["default", "onesided", "twosided"] = ..., scale_by_freq: bool | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray]: ... def specgram( self, x: ArrayLike, + *, NFFT: int | None = ..., Fs: float | None = ..., Fc: int | None = ..., @@ -717,13 +719,13 @@ class Axes(_AxesBase): scale: Literal["default", "linear", "dB"] | None = ..., vmin: float | None = ..., vmax: float | None = ..., - *, data=..., **kwargs ) -> tuple[np.ndarray, np.ndarray, np.ndarray, AxesImage]: ... def spy( self, Z: ArrayLike, + *, precision: float | Literal["present"] = ..., marker: str | None = ..., markersize: float | None = ..., @@ -736,6 +738,7 @@ class Axes(_AxesBase): self, dataset: ArrayLike | Sequence[ArrayLike], positions: ArrayLike | None = ..., + *, vert: bool = ..., widths: float | ArrayLike = ..., showmeans: bool = ..., @@ -748,13 +751,13 @@ class Axes(_AxesBase): | Callable[[GaussianKDE], float] | None = ..., side: Literal["both", "low", "high"] = ..., - *, data=..., ) -> dict[str, Collection]: ... def violin( self, vpstats: Sequence[dict[str, Any]], positions: ArrayLike | None = ..., + *, vert: bool = ..., widths: float | ArrayLike = ..., showmeans: bool = ..., From 163758a22bcdfb48223cb19bf4a8da67fb3da0ca Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 13 Feb 2024 23:28:59 +0100 Subject: [PATCH 2/2] Add note that make_kyword_only() must be the outer most decorator ... to the error message, that gets thrown if it is not. --- lib/matplotlib/_api/deprecation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/_api/deprecation.py b/lib/matplotlib/_api/deprecation.py index 283a55f1beb0..e9722f5d26c4 100644 --- a/lib/matplotlib/_api/deprecation.py +++ b/lib/matplotlib/_api/deprecation.py @@ -437,7 +437,8 @@ def make_keyword_only(since, name, func=None): assert (name in signature.parameters and signature.parameters[name].kind == POK), ( f"Matplotlib internal error: {name!r} must be a positional-or-keyword " - f"parameter for {func.__name__}()") + f"parameter for {func.__name__}(). If this error happens on a function with a " + f"pyplot wrapper, make sure make_keyword_only() is the outermost decorator.") names = [*signature.parameters] name_idx = names.index(name) kwonly = [name for name in names[name_idx:]