From 50cad2a347ff0beb4e2d322b6d54ff4cc650348f Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Fri, 28 Feb 2025 12:51:08 +0100
Subject: [PATCH 01/53] Backport PR #29584: DOC: Recommend constrained_layout
over tight_layout
---
galleries/users_explain/axes/tight_layout_guide.py | 10 ++++++----
lib/matplotlib/layout_engine.py | 11 ++++++++---
2 files changed, 14 insertions(+), 7 deletions(-)
diff --git a/galleries/users_explain/axes/tight_layout_guide.py b/galleries/users_explain/axes/tight_layout_guide.py
index 455bb57de126..672704bb9726 100644
--- a/galleries/users_explain/axes/tight_layout_guide.py
+++ b/galleries/users_explain/axes/tight_layout_guide.py
@@ -9,15 +9,17 @@
How to use tight-layout to fit plots within your figure cleanly.
+.. tip::
+
+ *tight_layout* was the first layout engine in Matplotlib. The more modern
+ and more capable :ref:`Constrained Layout ` should
+ typically be used instead.
+
*tight_layout* automatically adjusts subplot params so that the
subplot(s) fits in to the figure area. This is an experimental
feature and may not work for some cases. It only checks the extents
of ticklabels, axis labels, and titles.
-An alternative to *tight_layout* is :ref:`constrained_layout
-`.
-
-
Simple example
==============
diff --git a/lib/matplotlib/layout_engine.py b/lib/matplotlib/layout_engine.py
index 5a96745d0697..8a3276b53371 100644
--- a/lib/matplotlib/layout_engine.py
+++ b/lib/matplotlib/layout_engine.py
@@ -10,9 +10,14 @@
layout engine while the figure is being created. In particular, colorbars are
made differently with different layout engines (for historical reasons).
-Matplotlib supplies two layout engines, `.TightLayoutEngine` and
-`.ConstrainedLayoutEngine`. Third parties can create their own layout engine
-by subclassing `.LayoutEngine`.
+Matplotlib has two built-in layout engines:
+
+- `.TightLayoutEngine` was the first layout engine added to Matplotlib.
+ See also :ref:`tight_layout_guide`.
+- `.ConstrainedLayoutEngine` is more modern and generally gives better results.
+ See also :ref:`constrainedlayout_guide`.
+
+Third parties can create their own layout engine by subclassing `.LayoutEngine`.
"""
from contextlib import nullcontext
From 1bdc36aea428696118679f3d1d1a86180cddef5f Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Fri, 28 Feb 2025 16:03:49 +0100
Subject: [PATCH 02/53] Backport PR #29689: Fix alt and caption handling in
Sphinx directives
---
lib/matplotlib/sphinxext/figmpl_directive.py | 78 +++++++++----------
lib/matplotlib/sphinxext/plot_directive.py | 5 +-
lib/matplotlib/tests/test_sphinxext.py | 19 +++--
lib/matplotlib/tests/tinypages/some_plots.rst | 7 +-
4 files changed, 59 insertions(+), 50 deletions(-)
diff --git a/lib/matplotlib/sphinxext/figmpl_directive.py b/lib/matplotlib/sphinxext/figmpl_directive.py
index 5ef34f4dd0b1..7cb9c6c04e8a 100644
--- a/lib/matplotlib/sphinxext/figmpl_directive.py
+++ b/lib/matplotlib/sphinxext/figmpl_directive.py
@@ -12,16 +12,14 @@
See the *FigureMpl* documentation below.
"""
-from docutils import nodes
-
-from docutils.parsers.rst import directives
-from docutils.parsers.rst.directives.images import Figure, Image
-
import os
from os.path import relpath
from pathlib import PurePath, Path
import shutil
+from docutils import nodes
+from docutils.parsers.rst import directives
+from docutils.parsers.rst.directives.images import Figure, Image
from sphinx.errors import ExtensionError
import matplotlib
@@ -193,12 +191,13 @@ def visit_figmpl_html(self, node):
# make uri also be relative...
nm = PurePath(node['uri'][1:]).name
uri = f'{imagerel}/{rel}{nm}'
+ img_attrs = {'src': uri, 'alt': node['alt']}
# make srcset str. Need to change all the prefixes!
maxsrc = uri
- srcsetst = ''
if srcset:
maxmult = -1
+ srcsetst = ''
for mult, src in srcset.items():
nm = PurePath(src[1:]).name
# ../../_images/plot_1_2_0x.png
@@ -214,44 +213,43 @@ def visit_figmpl_html(self, node):
maxsrc = path
# trim trailing comma and space...
- srcsetst = srcsetst[:-2]
+ img_attrs['srcset'] = srcsetst[:-2]
- alt = node['alt']
if node['class'] is not None:
- classst = ' '.join(node['class'])
- classst = f'class="{classst}"'
-
- else:
- classst = ''
-
- stylers = ['width', 'height', 'scale']
- stylest = ''
- for style in stylers:
+ img_attrs['class'] = ' '.join(node['class'])
+ for style in ['width', 'height', 'scale']:
if node[style]:
- stylest += f'{style}: {node[style]};'
-
- figalign = node['align'] if node['align'] else 'center'
-
-#
-#
-#
-#
-#
-#
\n')
+ self.body.append('\n')
def visit_figmpl_latex(self, node):
diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py
index 65b25fb913a5..ab9b7d769550 100644
--- a/lib/matplotlib/sphinxext/plot_directive.py
+++ b/lib/matplotlib/sphinxext/plot_directive.py
@@ -876,7 +876,7 @@ def run(arguments, content, options, state_machine, state, lineno):
# Properly indent the caption
if caption and config.plot_srcset:
- caption = f':caption: {caption}'
+ caption = ':caption: ' + caption.replace('\n', ' ')
elif caption:
caption = '\n' + '\n'.join(' ' + line.strip()
for line in caption.split('\n'))
@@ -896,6 +896,9 @@ def run(arguments, content, options, state_machine, state, lineno):
if nofigs:
images = []
+ if 'alt' in options:
+ options['alt'] = options['alt'].replace('\n', ' ')
+
opts = [
f':{key}: {val}' for key, val in options.items()
if key in ('alt', 'height', 'width', 'scale', 'align', 'class')]
diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py
index 6e7b5ec5e50e..1aaa6baca47c 100644
--- a/lib/matplotlib/tests/test_sphinxext.py
+++ b/lib/matplotlib/tests/test_sphinxext.py
@@ -75,22 +75,25 @@ def plot_directive_file(num):
# Plot 13 shows close-figs in action
assert filecmp.cmp(range_4, plot_file(13))
# Plot 14 has included source
- html_contents = (html_dir / 'some_plots.html').read_bytes()
+ html_contents = (html_dir / 'some_plots.html').read_text(encoding='utf-8')
- assert b'# Only a comment' in html_contents
+ assert '# Only a comment' in html_contents
# check plot defined in external file.
assert filecmp.cmp(range_4, img_dir / 'range4.png')
assert filecmp.cmp(range_6, img_dir / 'range6_range6.png')
# check if figure caption made it into html file
- assert b'This is the caption for plot 15.' in html_contents
- # check if figure caption using :caption: made it into html file
- assert b'Plot 17 uses the caption option.' in html_contents
+ assert 'This is the caption for plot 15.' in html_contents
+ # check if figure caption using :caption: made it into html file (because this plot
+ # doesn't use srcset, the caption preserves newlines in the output.)
+ assert 'Plot 17 uses the caption option,\nwith multi-line input.' in html_contents
+ # check if figure alt text using :alt: made it into html file
+ assert 'Plot 17 uses the alt option, with multi-line input.' in html_contents
# check if figure caption made it into html file
- assert b'This is the caption for plot 18.' in html_contents
+ assert 'This is the caption for plot 18.' in html_contents
# check if the custom classes made it into the html file
- assert b'plot-directive my-class my-other-class' in html_contents
+ assert 'plot-directive my-class my-other-class' in html_contents
# check that the multi-image caption is applied twice
- assert html_contents.count(b'This caption applies to both plots.') == 2
+ assert html_contents.count('This caption applies to both plots.') == 2
# Plot 21 is range(6) plot via an include directive. But because some of
# the previous plots are repeated, the argument to plot_file() is only 17.
assert filecmp.cmp(range_6, plot_file(17))
diff --git a/lib/matplotlib/tests/tinypages/some_plots.rst b/lib/matplotlib/tests/tinypages/some_plots.rst
index dd1f79892b0e..cb56c5b3b8d5 100644
--- a/lib/matplotlib/tests/tinypages/some_plots.rst
+++ b/lib/matplotlib/tests/tinypages/some_plots.rst
@@ -135,7 +135,12 @@ Plot 16 uses a specific function in a file with plot commands:
Plot 17 gets a caption specified by the :caption: option:
.. plot::
- :caption: Plot 17 uses the caption option.
+ :caption:
+ Plot 17 uses the caption option,
+ with multi-line input.
+ :alt:
+ Plot 17 uses the alt option,
+ with multi-line input.
plt.figure()
plt.plot(range(6))
From bf640547c0cd6a4b0fb7634e127430a30d9c5b49 Mon Sep 17 00:00:00 2001
From: Praful Gulani <59774145+prafulgulani@users.noreply.github.com>
Date: Fri, 28 Feb 2025 20:54:41 +0530
Subject: [PATCH 03/53] Backport PR #29590: Blocked set_clim() callbacks to
prevent inconsistent state (#29522)
---
lib/matplotlib/colorizer.py | 21 ++++++++++++++++-----
lib/matplotlib/tests/test_colors.py | 17 +++++++++++++++++
2 files changed, 33 insertions(+), 5 deletions(-)
diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py
index 4aebe7d0f5dc..b4223f389804 100644
--- a/lib/matplotlib/colorizer.py
+++ b/lib/matplotlib/colorizer.py
@@ -261,16 +261,27 @@ def set_clim(self, vmin=None, vmax=None):
.. ACCEPTS: (vmin: float, vmax: float)
"""
# If the norm's limits are updated self.changed() will be called
- # through the callbacks attached to the norm
+ # through the callbacks attached to the norm, this causes an inconsistent
+ # state, to prevent this blocked context manager is used
if vmax is None:
try:
vmin, vmax = vmin
except (TypeError, ValueError):
pass
- if vmin is not None:
- self.norm.vmin = colors._sanitize_extrema(vmin)
- if vmax is not None:
- self.norm.vmax = colors._sanitize_extrema(vmax)
+
+ orig_vmin_vmax = self.norm.vmin, self.norm.vmax
+
+ # Blocked context manager prevents callbacks from being triggered
+ # until both vmin and vmax are updated
+ with self.norm.callbacks.blocked(signal='changed'):
+ if vmin is not None:
+ self.norm.vmin = colors._sanitize_extrema(vmin)
+ if vmax is not None:
+ self.norm.vmax = colors._sanitize_extrema(vmax)
+
+ # emit a update signal if the limits are changed
+ if orig_vmin_vmax != (self.norm.vmin, self.norm.vmax):
+ self.norm.callbacks.process('changed')
def get_clim(self):
"""
diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py
index cc6cb1bb11a7..1d13b940026b 100644
--- a/lib/matplotlib/tests/test_colors.py
+++ b/lib/matplotlib/tests/test_colors.py
@@ -1553,6 +1553,23 @@ def test_norm_deepcopy():
assert norm2.vmin == norm.vmin
+def test_set_clim_emits_single_callback():
+ data = np.array([[1, 2], [3, 4]])
+ fig, ax = plt.subplots()
+ image = ax.imshow(data, cmap='viridis')
+
+ callback = unittest.mock.Mock()
+ image.norm.callbacks.connect('changed', callback)
+
+ callback.assert_not_called()
+
+ # Call set_clim() to update the limits
+ image.set_clim(1, 5)
+
+ # Assert that only one "changed" callback is sent after calling set_clim()
+ callback.assert_called_once()
+
+
def test_norm_callback():
increment = unittest.mock.Mock(return_value=None)
From 7bbf2615e30dd687bd1af6e63444ae81894040ec Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Thu, 6 Mar 2025 11:30:50 +0100
Subject: [PATCH 04/53] Backport PR #29708: MNT: correct version in plotting
method deprecation warnings
---
lib/matplotlib/axes/_axes.py | 44 ++++++++++++++++++------------------
1 file changed, 22 insertions(+), 22 deletions(-)
diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py
index eebbdcaed459..85b2c1c2ad87 100644
--- a/lib/matplotlib/axes/_axes.py
+++ b/lib/matplotlib/axes/_axes.py
@@ -1095,7 +1095,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")
+ @_api.make_keyword_only("3.10", "label")
@_preprocess_data(replace_names=["y", "xmin", "xmax", "colors"],
label_namer="y")
def hlines(self, y, xmin, xmax, colors=None, linestyles='solid',
@@ -1187,7 +1187,7 @@ def hlines(self, y, xmin, xmax, colors=None, linestyles='solid',
self._request_autoscale_view()
return lines
- @_api.make_keyword_only("3.9", "label")
+ @_api.make_keyword_only("3.10", "label")
@_preprocess_data(replace_names=["x", "ymin", "ymax", "colors"],
label_namer="x")
def vlines(self, x, ymin, ymax, colors=None, linestyles='solid',
@@ -1279,7 +1279,7 @@ def vlines(self, x, ymin, ymax, colors=None, linestyles='solid',
self._request_autoscale_view()
return lines
- @_api.make_keyword_only("3.9", "orientation")
+ @_api.make_keyword_only("3.10", "orientation")
@_preprocess_data(replace_names=["positions", "lineoffsets",
"linelengths", "linewidths",
"colors", "linestyles"])
@@ -2086,7 +2086,7 @@ def acorr(self, x, **kwargs):
"""
return self.xcorr(x, x, **kwargs)
- @_api.make_keyword_only("3.9", "normed")
+ @_api.make_keyword_only("3.10", "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):
@@ -3229,7 +3229,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")
+ @_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,
@@ -3509,7 +3509,7 @@ def _errorevery_to_mask(x, errorevery):
everymask[errorevery] = True
return everymask
- @_api.make_keyword_only("3.9", "ecolor")
+ @_api.make_keyword_only("3.10", "ecolor")
@_preprocess_data(replace_names=["x", "y", "xerr", "yerr"],
label_namer="y")
@_docstring.interpd
@@ -3887,7 +3887,7 @@ def apply_mask(arrays, mask):
return errorbar_container # (l0, caplines, barcols)
- @_api.make_keyword_only("3.9", "notch")
+ @_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,
@@ -4236,7 +4236,7 @@ def boxplot(self, x, notch=None, sym=None, vert=None,
orientation=orientation)
return artists
- @_api.make_keyword_only("3.9", "widths")
+ @_api.make_keyword_only("3.10", "widths")
def bxp(self, bxpstats, positions=None, widths=None, vert=None,
orientation='vertical', patch_artist=False, shownotches=False,
showmeans=False, showcaps=True, showbox=True, showfliers=True,
@@ -4773,7 +4773,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")
+ @_api.make_keyword_only("3.10", "marker")
@_preprocess_data(replace_names=["x", "y", "s", "linewidths",
"edgecolors", "c", "facecolor",
"facecolors", "color"],
@@ -5064,7 +5064,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")
+ @_api.make_keyword_only("3.10", "gridsize")
@_preprocess_data(replace_names=["x", "y", "C"], label_namer="y")
@_docstring.interpd
def hexbin(self, x, y, C=None, gridsize=100, bins=None,
@@ -6814,7 +6814,7 @@ def clabel(self, CS, levels=None, **kwargs):
#### Data analysis
- @_api.make_keyword_only("3.9", "range")
+ @_api.make_keyword_only("3.10", "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',
@@ -7405,7 +7405,7 @@ def stairs(self, values, edges=None, *,
self._request_autoscale_view()
return patch
- @_api.make_keyword_only("3.9", "range")
+ @_api.make_keyword_only("3.10", "range")
@_preprocess_data(replace_names=["x", "y", "weights"])
@_docstring.interpd
def hist2d(self, x, y, bins=10, range=None, density=False, weights=None,
@@ -7617,7 +7617,7 @@ def ecdf(self, x, weights=None, *, complementary=False,
line.sticky_edges.x[:] = [0, 1]
return line
- @_api.make_keyword_only("3.9", "NFFT")
+ @_api.make_keyword_only("3.10", "NFFT")
@_preprocess_data(replace_names=["x"])
@_docstring.interpd
def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None,
@@ -7729,7 +7729,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")
+ @_api.make_keyword_only("3.10", "NFFT")
@_preprocess_data(replace_names=["x", "y"], label_namer="y")
@_docstring.interpd
def csd(self, x, y, NFFT=None, Fs=None, Fc=None, detrend=None,
@@ -7832,7 +7832,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")
+ @_api.make_keyword_only("3.10", "Fs")
@_preprocess_data(replace_names=["x"])
@_docstring.interpd
def magnitude_spectrum(self, x, Fs=None, Fc=None, window=None,
@@ -7919,7 +7919,7 @@ def magnitude_spectrum(self, x, Fs=None, Fc=None, window=None,
return spec, freqs, line
- @_api.make_keyword_only("3.9", "Fs")
+ @_api.make_keyword_only("3.10", "Fs")
@_preprocess_data(replace_names=["x"])
@_docstring.interpd
def angle_spectrum(self, x, Fs=None, Fc=None, window=None,
@@ -7989,7 +7989,7 @@ def angle_spectrum(self, x, Fs=None, Fc=None, window=None,
return spec, freqs, lines[0]
- @_api.make_keyword_only("3.9", "Fs")
+ @_api.make_keyword_only("3.10", "Fs")
@_preprocess_data(replace_names=["x"])
@_docstring.interpd
def phase_spectrum(self, x, Fs=None, Fc=None, window=None,
@@ -8059,7 +8059,7 @@ def phase_spectrum(self, x, Fs=None, Fc=None, window=None,
return spec, freqs, lines[0]
- @_api.make_keyword_only("3.9", "NFFT")
+ @_api.make_keyword_only("3.10", "NFFT")
@_preprocess_data(replace_names=["x", "y"])
@_docstring.interpd
def cohere(self, x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none,
@@ -8124,7 +8124,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")
+ @_api.make_keyword_only("3.10", "NFFT")
@_preprocess_data(replace_names=["x"])
@_docstring.interpd
def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None,
@@ -8286,7 +8286,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")
+ @_api.make_keyword_only("3.10", "precision")
@_docstring.interpd
def spy(self, Z, precision=0, marker=None, markersize=None,
aspect='equal', origin="upper", **kwargs):
@@ -8477,7 +8477,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")
+ @_api.make_keyword_only("3.10", "vert")
@_preprocess_data(replace_names=["dataset"])
def violinplot(self, dataset, positions=None, vert=None,
orientation='vertical', widths=0.5, showmeans=False,
@@ -8603,7 +8603,7 @@ def _kde_method(X, coords):
showmeans=showmeans, showextrema=showextrema,
showmedians=showmedians, side=side)
- @_api.make_keyword_only("3.9", "vert")
+ @_api.make_keyword_only("3.10", "vert")
def violin(self, vpstats, positions=None, vert=None,
orientation='vertical', widths=0.5, showmeans=False,
showextrema=True, showmedians=False, side='both'):
From 6843f812c87dbc1d9865d16475109f4469fffbe1 Mon Sep 17 00:00:00 2001
From: hannah
Date: Mon, 10 Mar 2025 14:39:24 -0400
Subject: [PATCH 05/53] Backport PR #29726: Add reference tag to Hatch style
reference
---
.../examples/shapes_and_collections/hatch_style_reference.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/galleries/examples/shapes_and_collections/hatch_style_reference.py b/galleries/examples/shapes_and_collections/hatch_style_reference.py
index 724abde051b4..e0409d989e6b 100644
--- a/galleries/examples/shapes_and_collections/hatch_style_reference.py
+++ b/galleries/examples/shapes_and_collections/hatch_style_reference.py
@@ -62,3 +62,7 @@ def hatches_plot(ax, h):
# - `matplotlib.patches.Rectangle`
# - `matplotlib.axes.Axes.add_patch`
# - `matplotlib.axes.Axes.text`
+#
+# .. tags::
+#
+# purpose: reference
From f098eb29cc4169b627fc32c9238e308aa434a376 Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Mon, 10 Mar 2025 23:36:49 +0100
Subject: [PATCH 06/53] Backport PR #29724: Fix SubplotSpec.get_gridspec type
hint
---
lib/matplotlib/gridspec.pyi | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/matplotlib/gridspec.pyi b/lib/matplotlib/gridspec.pyi
index 08c4dd7f4e49..3bf13ee17c4e 100644
--- a/lib/matplotlib/gridspec.pyi
+++ b/lib/matplotlib/gridspec.pyi
@@ -115,7 +115,7 @@ class SubplotSpec:
def num2(self) -> int: ...
@num2.setter
def num2(self, value: int) -> None: ...
- def get_gridspec(self) -> GridSpec: ...
+ def get_gridspec(self) -> GridSpecBase: ...
def get_geometry(self) -> tuple[int, int, int, int]: ...
@property
def rowspan(self) -> range: ...
From 6bc61bb878104dbc8526817e0334fed61370165f Mon Sep 17 00:00:00 2001
From: hannah
Date: Tue, 11 Mar 2025 20:23:22 -0400
Subject: [PATCH 07/53] Backport PR #29719: Fix passing singleton sequence-type
styles to hist
---
lib/matplotlib/axes/_axes.py | 21 ++++++++++++++++-----
lib/matplotlib/collections.py | 11 +----------
lib/matplotlib/lines.py | 14 ++++++++++++++
lib/matplotlib/tests/test_axes.py | 21 +++++++++++++++++++++
4 files changed, 52 insertions(+), 15 deletions(-)
diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py
index 85b2c1c2ad87..4b51049ebb44 100644
--- a/lib/matplotlib/axes/_axes.py
+++ b/lib/matplotlib/axes/_axes.py
@@ -7273,15 +7273,26 @@ def hist(self, x, bins=None, range=None, density=False, weights=None,
labels = [] if label is None else np.atleast_1d(np.asarray(label, str))
if histtype == "step":
- edgecolors = itertools.cycle(np.atleast_1d(kwargs.get('edgecolor',
- colors)))
+ ec = kwargs.get('edgecolor', colors)
else:
- edgecolors = itertools.cycle(np.atleast_1d(kwargs.get("edgecolor", None)))
+ ec = kwargs.get('edgecolor', None)
+ if ec is None or cbook._str_lower_equal(ec, 'none'):
+ edgecolors = itertools.repeat(ec)
+ else:
+ edgecolors = itertools.cycle(mcolors.to_rgba_array(ec))
+
+ fc = kwargs.get('facecolor', colors)
+ if cbook._str_lower_equal(fc, 'none'):
+ facecolors = itertools.repeat(fc)
+ else:
+ facecolors = itertools.cycle(mcolors.to_rgba_array(fc))
- facecolors = itertools.cycle(np.atleast_1d(kwargs.get('facecolor', colors)))
hatches = itertools.cycle(np.atleast_1d(kwargs.get('hatch', None)))
linewidths = itertools.cycle(np.atleast_1d(kwargs.get('linewidth', None)))
- linestyles = itertools.cycle(np.atleast_1d(kwargs.get('linestyle', None)))
+ if 'linestyle' in kwargs:
+ linestyles = itertools.cycle(mlines._get_dash_patterns(kwargs['linestyle']))
+ else:
+ linestyles = itertools.repeat(None)
for patch, lbl in itertools.zip_longest(patches, labels):
if not patch:
diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py
index b94410bcc140..9bc25913ee0e 100644
--- a/lib/matplotlib/collections.py
+++ b/lib/matplotlib/collections.py
@@ -626,17 +626,8 @@ def set_linestyle(self, ls):
':', '', (offset, on-off-seq)}. See `.Line2D.set_linestyle` for a
complete description.
"""
- try:
- dashes = [mlines._get_dash_pattern(ls)]
- except ValueError:
- try:
- dashes = [mlines._get_dash_pattern(x) for x in ls]
- except ValueError as err:
- emsg = f'Do not know how to convert {ls!r} to dashes'
- raise ValueError(emsg) from err
-
# get the list of raw 'unscaled' dash patterns
- self._us_linestyles = dashes
+ self._us_linestyles = mlines._get_dash_patterns(ls)
# broadcast and scale the lw and dash patterns
self._linewidths, self._linestyles = self._bcast_lwls(
diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py
index 65a4ccb6d950..21dd91b89f49 100644
--- a/lib/matplotlib/lines.py
+++ b/lib/matplotlib/lines.py
@@ -60,6 +60,20 @@ def _get_dash_pattern(style):
return offset, dashes
+def _get_dash_patterns(styles):
+ """Convert linestyle or sequence of linestyles to list of dash patterns."""
+ try:
+ patterns = [_get_dash_pattern(styles)]
+ except ValueError:
+ try:
+ patterns = [_get_dash_pattern(x) for x in styles]
+ except ValueError as err:
+ emsg = f'Do not know how to convert {styles!r} to dashes'
+ raise ValueError(emsg) from err
+
+ return patterns
+
+
def _get_inverse_dash_pattern(offset, dashes):
"""Return the inverse of the given dash pattern, for filling the gaps."""
# Define the inverse pattern by moving the last gap to the start of the
diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py
index cd5cd08fbf74..952ccf3a3636 100644
--- a/lib/matplotlib/tests/test_axes.py
+++ b/lib/matplotlib/tests/test_axes.py
@@ -4762,6 +4762,27 @@ def test_hist_vectorized_params(fig_test, fig_ref, kwargs):
zorder=(len(xs)-i)/2)
+def test_hist_sequence_type_styles():
+ facecolor = ('r', 0.5)
+ edgecolor = [0.5, 0.5, 0.5]
+ linestyle = (0, (1, 1))
+
+ arr = np.random.uniform(size=50)
+ _, _, bars = plt.hist(arr, facecolor=facecolor, edgecolor=edgecolor,
+ linestyle=linestyle)
+ assert mcolors.same_color(bars[0].get_facecolor(), facecolor)
+ assert mcolors.same_color(bars[0].get_edgecolor(), edgecolor)
+ assert bars[0].get_linestyle() == linestyle
+
+
+def test_hist_color_none():
+ arr = np.random.uniform(size=50)
+ # No edgecolor is the default but check that it can be explicitly passed.
+ _, _, bars = plt.hist(arr, facecolor='none', edgecolor='none')
+ assert bars[0].get_facecolor(), (0, 0, 0, 0)
+ assert bars[0].get_edgecolor(), (0, 0, 0, 0)
+
+
@pytest.mark.parametrize('kwargs, patch_face, patch_edge',
# 'C0'(blue) stands for the first color of the
# default color cycle as well as the patch.facecolor rcParam
From 3ac78ff70b89ba1fe18a118ef5681356b89a24b2 Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Wed, 12 Mar 2025 13:01:26 +0100
Subject: [PATCH 08/53] Backport PR #29734: ci: MacOS 14: temporarily
upper-bound the 'PyGObject' Python package version
---
.github/workflows/tests.yml | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index d42ac6ea00a2..e856eef76c6e 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -103,10 +103,14 @@ jobs:
pyside6-ver: '!=6.5.1'
- os: macos-14 # This runner is on M1 (arm64) chips.
python-version: '3.12'
+ # https://github.com/matplotlib/matplotlib/issues/29732
+ pygobject-ver: '<3.52.0'
# https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346
pyside6-ver: '!=6.5.1'
- os: macos-14 # This runner is on M1 (arm64) chips.
python-version: '3.13'
+ # https://github.com/matplotlib/matplotlib/issues/29732
+ pygobject-ver: '<3.52.0'
# https://bugreports.qt.io/projects/PYSIDE/issues/PYSIDE-2346
pyside6-ver: '!=6.5.1'
@@ -270,7 +274,7 @@ jobs:
# (sometimes, the install appears to be successful but shared
# libraries cannot be loaded at runtime, so an actual import is a
# better check).
- python -m pip install --upgrade pycairo 'cairocffi>=0.8' PyGObject &&
+ python -m pip install --upgrade pycairo 'cairocffi>=0.8' PyGObject${{ matrix.pygobject-ver }} &&
(
python -c 'import gi; gi.require_version("Gtk", "4.0"); from gi.repository import Gtk' &&
echo 'PyGObject 4 is available' || echo 'PyGObject 4 is not available'
From d0ab4681ae5db8c971f1e1f3b4b27bc9834a3e32 Mon Sep 17 00:00:00 2001
From: Thomas A Caswell
Date: Thu, 13 Mar 2025 15:14:15 -0400
Subject: [PATCH 09/53] Backport PR #29748: Fix PyGObject version pinning in
macOS tests
---
.github/workflows/tests.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index e856eef76c6e..376aaa3e293d 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -274,7 +274,7 @@ jobs:
# (sometimes, the install appears to be successful but shared
# libraries cannot be loaded at runtime, so an actual import is a
# better check).
- python -m pip install --upgrade pycairo 'cairocffi>=0.8' PyGObject${{ matrix.pygobject-ver }} &&
+ python -m pip install --upgrade pycairo 'cairocffi>=0.8' 'PyGObject${{ matrix.pygobject-ver }}' &&
(
python -c 'import gi; gi.require_version("Gtk", "4.0"); from gi.repository import Gtk' &&
echo 'PyGObject 4 is available' || echo 'PyGObject 4 is not available'
From 5090b264dbc60899ecef0cd0e0e4252668bd0542 Mon Sep 17 00:00:00 2001
From: Ian Thomas
Date: Fri, 14 Mar 2025 08:38:45 +0000
Subject: [PATCH 10/53] Backport PR #29721: FIX: pyplot auto-backend detection
case-sensitivity fixup
---
lib/matplotlib/pyplot.py | 15 +++++++++------
lib/matplotlib/tests/test_rcparams.py | 25 ++++++++++++++++++++++++-
2 files changed, 33 insertions(+), 7 deletions(-)
diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py
index 48aea1b3bd9c..83b380505d5b 100644
--- a/lib/matplotlib/pyplot.py
+++ b/lib/matplotlib/pyplot.py
@@ -2715,12 +2715,15 @@ def polar(*args, **kwargs) -> list[Line2D]:
# If rcParams['backend_fallback'] is true, and an interactive backend is
# requested, ignore rcParams['backend'] and force selection of a backend that
# is compatible with the current running interactive framework.
-if (rcParams["backend_fallback"]
- and rcParams._get_backend_or_none() in ( # type: ignore[attr-defined]
- set(backend_registry.list_builtin(BackendFilter.INTERACTIVE)) -
- {'webagg', 'nbagg'})
- and cbook._get_running_interactive_framework()):
- rcParams._set("backend", rcsetup._auto_backend_sentinel)
+if rcParams["backend_fallback"]:
+ requested_backend = rcParams._get_backend_or_none() # type: ignore[attr-defined]
+ requested_backend = None if requested_backend is None else requested_backend.lower()
+ available_backends = backend_registry.list_builtin(BackendFilter.INTERACTIVE)
+ if (
+ requested_backend in (set(available_backends) - {'webagg', 'nbagg'})
+ and cbook._get_running_interactive_framework()
+ ):
+ rcParams._set("backend", rcsetup._auto_backend_sentinel)
# fmt: on
diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py
index 13633956c349..bea5e90ea4e5 100644
--- a/lib/matplotlib/tests/test_rcparams.py
+++ b/lib/matplotlib/tests/test_rcparams.py
@@ -521,10 +521,11 @@ def test_rcparams_reset_after_fail():
@pytest.mark.skipif(sys.platform != "linux", reason="Linux only")
-def test_backend_fallback_headless(tmp_path):
+def test_backend_fallback_headless_invalid_backend(tmp_path):
env = {**os.environ,
"DISPLAY": "", "WAYLAND_DISPLAY": "",
"MPLBACKEND": "", "MPLCONFIGDIR": str(tmp_path)}
+ # plotting should fail with the tkagg backend selected in a headless environment
with pytest.raises(subprocess.CalledProcessError):
subprocess_run_for_testing(
[sys.executable, "-c",
@@ -536,6 +537,28 @@ def test_backend_fallback_headless(tmp_path):
env=env, check=True, stderr=subprocess.DEVNULL)
+@pytest.mark.skipif(sys.platform != "linux", reason="Linux only")
+def test_backend_fallback_headless_auto_backend(tmp_path):
+ # specify a headless mpl environment, but request a graphical (tk) backend
+ env = {**os.environ,
+ "DISPLAY": "", "WAYLAND_DISPLAY": "",
+ "MPLBACKEND": "TkAgg", "MPLCONFIGDIR": str(tmp_path)}
+
+ # allow fallback to an available interactive backend explicitly in configuration
+ rc_path = tmp_path / "matplotlibrc"
+ rc_path.write_text("backend_fallback: true")
+
+ # plotting should succeed, by falling back to use the generic agg backend
+ backend = subprocess_run_for_testing(
+ [sys.executable, "-c",
+ "import matplotlib.pyplot;"
+ "matplotlib.pyplot.plot(42);"
+ "print(matplotlib.get_backend());"
+ ],
+ env=env, text=True, check=True, capture_output=True).stdout
+ assert backend.strip().lower() == "agg"
+
+
@pytest.mark.skipif(
sys.platform == "linux" and not _c_internal_utils.xdisplay_is_valid(),
reason="headless")
From f86947c07bd4f2c04ce508dba76f88d4ea8e4bac Mon Sep 17 00:00:00 2001
From: Qian Zhang <88585542+QianZhang19@users.noreply.github.com>
Date: Tue, 18 Mar 2025 14:11:23 +0000
Subject: [PATCH 11/53] Backport PR #29767: Add description to logit_demo.py
script
---
galleries/examples/scales/logit_demo.py | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/galleries/examples/scales/logit_demo.py b/galleries/examples/scales/logit_demo.py
index 22a56433ccd7..e8d42fc35711 100644
--- a/galleries/examples/scales/logit_demo.py
+++ b/galleries/examples/scales/logit_demo.py
@@ -4,6 +4,20 @@
===========
Examples of plots with logit axes.
+
+This example visualises how ``set_yscale("logit")`` works on probability plots
+by generating three distributions: normal, laplacian, and cauchy in one plot.
+
+The advantage of logit scale is that it effectively spreads out values close to 0 and 1.
+
+In a linear scale plot, probability values near 0 and 1 appear compressed,
+making it difficult to see differences in those regions.
+
+In a logit scale plot, the transformation expands these regions,
+making the graph cleaner and easier to compare across different probability values.
+
+This makes the logit scale especially useful when visalising probabilities in logistic
+regression, classification models, and cumulative distribution functions.
"""
import math
From 89460857bc2ceaea390068a16e8e1ea812c8b4bc Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Tue, 18 Mar 2025 22:34:47 +0100
Subject: [PATCH 12/53] Backport PR #29752: DOC: Add install instructions for
pixi and uv
---
doc/index.rst | 28 ++++++++++++++++++++++++++++
doc/install/index.rst | 3 ++-
2 files changed, 30 insertions(+), 1 deletion(-)
diff --git a/doc/index.rst b/doc/index.rst
index dedd614985df..74a183d6cd7b 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -29,6 +29,34 @@ Install
conda install -c conda-forge matplotlib
+ .. tab-item:: pixi
+
+ .. code-block:: bash
+
+ pixi add matplotlib
+
+ .. tab-item:: uv
+
+ .. code-block:: bash
+
+ uv add matplotlib
+
+ .. warning::
+
+ If you install Python with ``uv`` then the ``tkagg`` backend
+ will not be available because python-build-standalone (used by uv
+ to distribute Python) does not contain tk bindings that are usable by
+ Matplotlib (see `this issue`_ for details). If you want Matplotlib
+ to be able to display plots in a window, you should install one of
+ the other :ref:`supported GUI frameworks `,
+ e.g.
+
+ .. code-block:: bash
+
+ uv add matplotlib pyside6
+
+ .. _this issue: https://github.com/astral-sh/uv/issues/6893#issuecomment-2565965851
+
.. tab-item:: other
.. rst-class:: section-toc
diff --git a/doc/install/index.rst b/doc/install/index.rst
index 99ccc163a82e..2d9e724e6a73 100644
--- a/doc/install/index.rst
+++ b/doc/install/index.rst
@@ -28,7 +28,8 @@ precompiled wheel for your OS and Python.
The following backends work out of the box: Agg, ps, pdf, svg
Python is typically shipped with tk bindings which are used by
- TkAgg.
+ TkAgg. Notably, python-build-standalone – used by ``uv`` – does
+ not include tk bindings that are usable by Matplotlib.
For support of other GUI frameworks, LaTeX rendering, saving
animations and a larger selection of file formats, you can
From 2ddb364a015d66a3f3c39fd42cb5f3083409f18d Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Thu, 20 Mar 2025 19:00:17 +0100
Subject: [PATCH 13/53] Backport PR #29781: Fix escaping of nulls and "0" in
default filenames.
---
lib/matplotlib/backend_bases.py | 17 +++++++++--------
lib/matplotlib/tests/test_backend_bases.py | 5 ++++-
2 files changed, 13 insertions(+), 9 deletions(-)
diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py
index 0843796bad5a..a889a6b26e8e 100644
--- a/lib/matplotlib/backend_bases.py
+++ b/lib/matplotlib/backend_bases.py
@@ -2218,7 +2218,7 @@ def get_default_filename(self):
# Characters to be avoided in a NT path:
# https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#naming_conventions
# plus ' '
- removed_chars = r'<>:"/\|?*\0 '
+ removed_chars = '<>:"/\\|?*\0 '
default_basename = default_basename.translate(
{ord(c): "_" for c in removed_chars})
default_filetype = self.get_default_filetype()
@@ -2728,23 +2728,24 @@ def resize(self, w, h):
"""For GUI backends, resize the window (in physical pixels)."""
def get_window_title(self):
- """
- Return the title text of the window containing the figure, or None
- if there is no window (e.g., a PS backend).
- """
- return 'image'
+ """Return the title text of the window containing the figure."""
+ return self._window_title
def set_window_title(self, title):
"""
Set the title text of the window containing the figure.
- This has no effect for non-GUI (e.g., PS) backends.
-
Examples
--------
>>> fig = plt.figure()
>>> fig.canvas.manager.set_window_title('My figure')
"""
+ # This attribute is not defined in __init__ (but __init__ calls this
+ # setter), as derived classes (real GUI managers) will store this
+ # information directly on the widget; only the base (non-GUI) manager
+ # class needs a specific attribute for it (so that filename escaping
+ # can be checked in the test suite).
+ self._window_title = title
cursors = tools.cursors
diff --git a/lib/matplotlib/tests/test_backend_bases.py b/lib/matplotlib/tests/test_backend_bases.py
index 3e1f524ed1c9..10108d11bd0a 100644
--- a/lib/matplotlib/tests/test_backend_bases.py
+++ b/lib/matplotlib/tests/test_backend_bases.py
@@ -64,7 +64,10 @@ def test_canvas_ctor():
def test_get_default_filename():
- assert plt.figure().canvas.get_default_filename() == 'image.png'
+ fig = plt.figure()
+ assert fig.canvas.get_default_filename() == "Figure_1.png"
+ fig.canvas.manager.set_window_title("0:1/2<3")
+ assert fig.canvas.get_default_filename() == "0_1_2_3.png"
def test_canvas_change():
From ac6495fa4255697c360775cf8cbdf6528678e28b Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Fri, 21 Mar 2025 08:24:14 +0100
Subject: [PATCH 14/53] Backport PR #29755: DOC: Simplify annotation arrow
style reference
---
.../fancyarrow_demo.py | 28 ++++++++-----------
1 file changed, 12 insertions(+), 16 deletions(-)
diff --git a/galleries/examples/text_labels_and_annotations/fancyarrow_demo.py b/galleries/examples/text_labels_and_annotations/fancyarrow_demo.py
index 8d0027831e4a..4f45a9a72a47 100644
--- a/galleries/examples/text_labels_and_annotations/fancyarrow_demo.py
+++ b/galleries/examples/text_labels_and_annotations/fancyarrow_demo.py
@@ -19,35 +19,31 @@
nrow = (len(styles) + 1) // ncol
axs = (plt.figure(figsize=(4 * ncol, 1 + nrow))
.add_gridspec(1 + nrow, ncol,
- wspace=.7, left=.1, right=.9, bottom=0, top=1).subplots())
+ wspace=0, hspace=0, left=0, right=1, bottom=0, top=1).subplots())
for ax in axs.flat:
+ ax.set_xlim(-0.5, 4)
ax.set_axis_off()
for ax in axs[0, :]:
- ax.text(0, .5, "arrowstyle",
- transform=ax.transAxes, size="large", color="tab:blue",
- horizontalalignment="center", verticalalignment="center")
- ax.text(.35, .5, "default parameters",
- transform=ax.transAxes,
- horizontalalignment="left", verticalalignment="center")
+ ax.text(-0.25, 0.5, "arrowstyle", size="large", color="tab:blue")
+ ax.text(1.25, .5, "default parameters", size="large")
for ax, (stylename, stylecls) in zip(axs[1:, :].T.flat, styles.items()):
- l, = ax.plot(.25, .5, "ok", transform=ax.transAxes)
- ax.annotate(stylename, (.25, .5), (-0.1, .5),
- xycoords="axes fraction", textcoords="axes fraction",
- size="large", color="tab:blue",
- horizontalalignment="center", verticalalignment="center",
+ # draw dot and annotation with arrowstyle
+ l, = ax.plot(1, 0, "o", color="grey")
+ ax.annotate(stylename, (1, 0), (0, 0),
+ size="large", color="tab:blue", ha="center", va="center",
arrowprops=dict(
arrowstyle=stylename, connectionstyle="arc3,rad=-0.05",
color="k", shrinkA=5, shrinkB=5, patchB=l,
),
- bbox=dict(boxstyle="square", fc="w"))
+ bbox=dict(boxstyle="square", fc="w", ec="grey"))
+ # draw default parameters
# wrap at every nth comma (n = 1 or 2, depending on text length)
s = str(inspect.signature(stylecls))[1:-1]
n = 2 if s.count(',') > 3 else 1
- ax.text(.35, .5,
+ ax.text(1.25, 0,
re.sub(', ', lambda m, c=itertools.count(1): m.group()
if next(c) % n else '\n', s),
- transform=ax.transAxes,
- horizontalalignment="left", verticalalignment="center")
+ verticalalignment="center")
plt.show()
From ba55b7cdac9d866faf970ce13e5c1d938e3f3a24 Mon Sep 17 00:00:00 2001
From: David Stansby
Date: Fri, 21 Mar 2025 19:58:34 +0000
Subject: [PATCH 15/53] Backport PR #29770: MNT: Move test for old ipython
behavior to minver tests
---
.github/workflows/tests.yml | 7 +------
requirements/testing/minver.txt | 8 ++++++++
2 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index e856eef76c6e..601305096fd7 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -59,12 +59,7 @@ jobs:
pyside6-ver: '==6.2.0'
- os: ubuntu-20.04
python-version: '3.10'
- # One CI run tests ipython/matplotlib-inline before backend mapping moved to mpl
- extra-requirements:
- -r requirements/testing/extra.txt
- "ipython==7.29.0"
- "ipykernel==5.5.6"
- "matplotlib-inline<0.1.7"
+ extra-requirements: '-r requirements/testing/extra.txt'
CFLAGS: "-fno-lto" # Ensure that disabling LTO works.
# https://github.com/matplotlib/matplotlib/pull/26052#issuecomment-1574595954
# https://www.riverbankcomputing.com/pipermail/pyqt/2023-November/045606.html
diff --git a/requirements/testing/minver.txt b/requirements/testing/minver.txt
index 3932e68eb015..d30ebf08f04b 100644
--- a/requirements/testing/minver.txt
+++ b/requirements/testing/minver.txt
@@ -13,3 +13,11 @@ pillow==8.3.2
pyparsing==2.3.1
pytest==7.0.0
python-dateutil==2.7
+
+# Test ipython/matplotlib-inline before backend mapping moved to mpl.
+# This should be tested for a reasonably long transition period,
+# but we will eventually remove the test when we no longer support
+# ipython/matplotlib-inline versions from before the transition.
+ipython==7.29.0
+ipykernel==5.5.6
+matplotlib-inline<0.1.7
From 64ff907d03e691173136ac2744c251491047683c Mon Sep 17 00:00:00 2001
From: Raphael Erik Hviding
Date: Fri, 14 Feb 2025 08:40:43 +0100
Subject: [PATCH 16/53] Backport PR #29552: Bug Fix: Normalize kwargs for
Histogram
---
lib/matplotlib/axes/_axes.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py
index 4b51049ebb44..d11f916f7429 100644
--- a/lib/matplotlib/axes/_axes.py
+++ b/lib/matplotlib/axes/_axes.py
@@ -7018,6 +7018,8 @@ def hist(self, x, bins=None, range=None, density=False, weights=None,
bin_range = range
from builtins import range
+ kwargs = cbook.normalize_kwargs(kwargs, mpatches.Patch)
+
if np.isscalar(x):
x = [x]
From 0359016299d562b5b90a6e3ae3ceeb5c9f51e1af Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Fri, 28 Mar 2025 00:14:24 +0100
Subject: [PATCH 17/53] Backport PR #29801: DOC: Slightly further improve
arrowstyle demo
---
.../fancyarrow_demo.py | 38 ++++++++++---------
1 file changed, 21 insertions(+), 17 deletions(-)
diff --git a/galleries/examples/text_labels_and_annotations/fancyarrow_demo.py b/galleries/examples/text_labels_and_annotations/fancyarrow_demo.py
index 4f45a9a72a47..6a2700d20515 100644
--- a/galleries/examples/text_labels_and_annotations/fancyarrow_demo.py
+++ b/galleries/examples/text_labels_and_annotations/fancyarrow_demo.py
@@ -3,7 +3,11 @@
Annotation arrow style reference
================================
-Overview of the arrow styles available in `~.Axes.annotate`.
+Overview of the available `.ArrowStyle` settings. These are used for the *arrowstyle*
+parameter of `~.Axes.annotate` and `.FancyArrowPatch`.
+
+Each style can be configured with a set of parameters, which are stated along with
+their default values.
"""
import inspect
@@ -12,38 +16,38 @@
import matplotlib.pyplot as plt
-import matplotlib.patches as mpatches
+from matplotlib.patches import ArrowStyle
-styles = mpatches.ArrowStyle.get_styles()
+styles = ArrowStyle.get_styles()
ncol = 2
nrow = (len(styles) + 1) // ncol
-axs = (plt.figure(figsize=(4 * ncol, 1 + nrow))
- .add_gridspec(1 + nrow, ncol,
- wspace=0, hspace=0, left=0, right=1, bottom=0, top=1).subplots())
+gridspec_kw = dict(wspace=0, hspace=0.05, left=0, right=1, bottom=0, top=1)
+fig, axs = plt.subplots(1 + nrow, ncol,
+ figsize=(4 * ncol, 1 + nrow), gridspec_kw=gridspec_kw)
for ax in axs.flat:
- ax.set_xlim(-0.5, 4)
+ ax.set_xlim(-0.1, 4)
ax.set_axis_off()
for ax in axs[0, :]:
- ax.text(-0.25, 0.5, "arrowstyle", size="large", color="tab:blue")
- ax.text(1.25, .5, "default parameters", size="large")
+ ax.text(0, 0.5, "arrowstyle", size="large", color="tab:blue")
+ ax.text(1.4, .5, "default parameters", size="large")
for ax, (stylename, stylecls) in zip(axs[1:, :].T.flat, styles.items()):
# draw dot and annotation with arrowstyle
- l, = ax.plot(1, 0, "o", color="grey")
- ax.annotate(stylename, (1, 0), (0, 0),
- size="large", color="tab:blue", ha="center", va="center",
+ l, = ax.plot(1.25, 0, "o", color="darkgrey")
+ ax.annotate(stylename, (1.25, 0), (0, 0),
+ size="large", color="tab:blue", va="center", family="monospace",
arrowprops=dict(
- arrowstyle=stylename, connectionstyle="arc3,rad=-0.05",
- color="k", shrinkA=5, shrinkB=5, patchB=l,
+ arrowstyle=stylename, connectionstyle="arc3,rad=0",
+ color="black", shrinkA=5, shrinkB=5, patchB=l,
),
- bbox=dict(boxstyle="square", fc="w", ec="grey"))
+ bbox=dict(boxstyle="square", fc="w", ec="darkgrey"))
# draw default parameters
# wrap at every nth comma (n = 1 or 2, depending on text length)
s = str(inspect.signature(stylecls))[1:-1]
n = 2 if s.count(',') > 3 else 1
- ax.text(1.25, 0,
+ ax.text(1.4, 0,
re.sub(', ', lambda m, c=itertools.count(1): m.group()
if next(c) % n else '\n', s),
- verticalalignment="center")
+ verticalalignment="center", color="0.3")
plt.show()
From 5fde149fa98b1ea4b56ad2a385e7b4f5743621af Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Tue, 1 Apr 2025 13:27:32 +0200
Subject: [PATCH 18/53] Backport PR #29839: Improve docs regarding plt.close().
---
doc/users/faq.rst | 1 +
lib/matplotlib/pyplot.py | 10 +++++++++-
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/doc/users/faq.rst b/doc/users/faq.rst
index 1ff21d739108..592fff551099 100644
--- a/doc/users/faq.rst
+++ b/doc/users/faq.rst
@@ -287,6 +287,7 @@ the desired format::
import matplotlib.pyplot as plt
plt.plot([1, 2, 3])
plt.savefig('myfig.png')
+ plt.close()
.. seealso::
diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py
index 83b380505d5b..9ef61f335c18 100644
--- a/lib/matplotlib/pyplot.py
+++ b/lib/matplotlib/pyplot.py
@@ -1170,7 +1170,7 @@ def disconnect(cid: int) -> None:
def close(fig: None | int | str | Figure | Literal["all"] = None) -> None:
"""
- Close a figure window.
+ Close a figure window, and unregister it from pyplot.
Parameters
----------
@@ -1183,6 +1183,14 @@ def close(fig: None | int | str | Figure | Literal["all"] = None) -> None:
- ``str``: a figure name
- 'all': all figures
+ Notes
+ -----
+ pyplot maintains a reference to figures created with `figure()`. When
+ work on the figure is completed, it should be closed, i.e. deregistered
+ from pyplot, to free its memory (see also :rc:figure.max_open_warning).
+ Closing a figure window created by `show()` automatically deregisters the
+ figure. For all other use cases, most prominently `savefig()` without
+ `show()`, the figure must be deregistered explicitly using `close()`.
"""
if fig is None:
manager = _pylab_helpers.Gcf.get_active()
From b29bd158779234ac9941bbefadf5f02d234183ea Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Wed, 2 Apr 2025 20:31:10 +0200
Subject: [PATCH 19/53] Backport PR #29842: Don't drag draggables on scroll
events
---
lib/matplotlib/offsetbox.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py
index 8dd5f4ec23c1..86223e65d6bb 100644
--- a/lib/matplotlib/offsetbox.py
+++ b/lib/matplotlib/offsetbox.py
@@ -1488,7 +1488,7 @@ def finalize_offset(self):
def __init__(self, ref_artist, use_blit=False):
self.ref_artist = ref_artist
if not ref_artist.pickable():
- ref_artist.set_picker(True)
+ ref_artist.set_picker(self._picker)
self.got_artist = False
self._use_blit = use_blit and self.canvas.supports_blit
callbacks = self.canvas.callbacks
@@ -1502,6 +1502,11 @@ def __init__(self, ref_artist, use_blit=False):
]
]
+ @staticmethod
+ def _picker(artist, mouseevent):
+ # A custom picker to prevent dragging on mouse scroll events
+ return (artist.contains(mouseevent) and mouseevent.name != "scroll_event"), {}
+
# A property, not an attribute, to maintain picklability.
canvas = property(lambda self: self.ref_artist.get_figure(root=True).canvas)
cids = property(lambda self: [
From 00b4df2178346e25b724a2e471800fd0444403b5 Mon Sep 17 00:00:00 2001
From: Elliott Sales de Andrade
Date: Thu, 3 Apr 2025 02:59:44 -0400
Subject: [PATCH 20/53] Backport PR #29545: DOC: correctly specify return type
of `figaspect`
---
lib/matplotlib/figure.py | 4 ++--
lib/matplotlib/figure.pyi | 4 +++-
lib/matplotlib/pyplot.py | 2 +-
3 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py
index 3d6f9a7f4c16..089141727189 100644
--- a/lib/matplotlib/figure.py
+++ b/lib/matplotlib/figure.py
@@ -3665,8 +3665,8 @@ def figaspect(arg):
Returns
-------
- width, height : float
- The figure size in inches.
+ size : (2,) array
+ The width and height of the figure in inches.
Notes
-----
diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi
index 08bf1505532b..064503c91384 100644
--- a/lib/matplotlib/figure.pyi
+++ b/lib/matplotlib/figure.pyi
@@ -418,4 +418,6 @@ class Figure(FigureBase):
rect: tuple[float, float, float, float] | None = ...
) -> None: ...
-def figaspect(arg: float | ArrayLike) -> tuple[float, float]: ...
+def figaspect(
+ arg: float | ArrayLike,
+) -> np.ndarray[tuple[Literal[2]], np.dtype[np.float64]]: ...
diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py
index 9ef61f335c18..1f50a05a54f7 100644
--- a/lib/matplotlib/pyplot.py
+++ b/lib/matplotlib/pyplot.py
@@ -873,7 +873,7 @@ def figure(
# autoincrement if None, else integer from 1-N
num: int | str | Figure | SubFigure | None = None,
# defaults to rc figure.figsize
- figsize: tuple[float, float] | None = None,
+ figsize: ArrayLike | None = None,
# defaults to rc figure.dpi
dpi: float | None = None,
*,
From a4996439aa0675c81fecd3382a4d281cbe675f3f Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Thu, 3 Apr 2025 13:38:47 +0200
Subject: [PATCH 21/53] Backport PR #29773: DOC: Improve interactive figures
guide / Blocking input
---
.../figure/interactive_guide.rst | 78 ++++++++++---------
1 file changed, 41 insertions(+), 37 deletions(-)
diff --git a/galleries/users_explain/figure/interactive_guide.rst b/galleries/users_explain/figure/interactive_guide.rst
index 5cc55edc0955..6cbdca4ab3f9 100644
--- a/galleries/users_explain/figure/interactive_guide.rst
+++ b/galleries/users_explain/figure/interactive_guide.rst
@@ -11,7 +11,7 @@ Interactive figures and asynchronous programming
Matplotlib supports rich interactive figures by embedding figures into
a GUI window. The basic interactions of panning and zooming in an
-Axes to inspect your data is "baked in" to Matplotlib. This is
+Axes to inspect your data is available out-of-the-box. This is
supported by a full mouse and keyboard event handling system that
you can use to build sophisticated interactive graphs.
@@ -23,6 +23,21 @@ handling system `, `Interactive Tutorial
`Interactive Applications using Matplotlib
`__.
+
+GUI events
+==========
+
+All GUI frameworks (Qt, Wx, Gtk, Tk, macOS, or web) have some method of
+capturing user interactions and passing them back to the application, but
+the exact details depend on the toolkit (for example callbacks in Tk or
+the ``Signal`` / ``Slot`` framework in Qt). The Matplotlib :ref:`backends
+` encapsulate the details of the GUI frameworks and
+provide a framework-independent interface to GUI events through Matplotlib's
+:ref:`event handling system `. By connecting functions
+to the event handling system (see `.FigureCanvasBase.mpl_connect`), you can
+interactively respond to user actions in a GUI toolkit agnostic way.
+
+
Event loops
===========
@@ -58,19 +73,6 @@ depending on the library, by methods with names like ``exec``,
``run``, or ``start``.
-All GUI frameworks (Qt, Wx, Gtk, tk, macOS, or web) have some method of
-capturing user interactions and passing them back to the application
-(for example ``Signal`` / ``Slot`` framework in Qt) but the exact
-details depend on the toolkit. Matplotlib has a :ref:`backend
-` for each GUI toolkit we support which uses the
-toolkit API to bridge the toolkit UI events into Matplotlib's :ref:`event
-handling system `. You can then use
-`.FigureCanvasBase.mpl_connect` to connect your function to
-Matplotlib's event handling system. This allows you to directly
-interact with your data and write GUI toolkit agnostic user
-interfaces.
-
-
.. _cp_integration:
Command prompt integration
@@ -81,16 +83,16 @@ lets us interactively send code to the interpreter and get results
back. We also have the GUI toolkit that runs an event loop waiting
for user input and lets us register functions to be run when that
happens. However, if we want to do both we have a problem: the prompt
-and the GUI event loop are both infinite loops that each think *they*
-are in charge! In order for both the prompt and the GUI windows to be
+and the GUI event loop are both infinite loops and cannot run in
+parallel. In order for both the prompt and the GUI windows to be
responsive we need a method to allow the loops to "timeshare" :
-1. let the GUI main loop block the python process when you want
- interactive windows
-2. let the CLI main loop block the python process and intermittently
- run the GUI loop
-3. fully embed python in the GUI (but this is basically writing a full
- application)
+1. **Blocking the prompt**: let the GUI main loop block the python
+ process when you want interactive windows
+2. **Input hook integration**: let the CLI main loop block the python
+ process and intermittently run the GUI loop
+3. **Full embedding**: fully embed python in the GUI
+ (but this is basically writing a full application)
.. _cp_block_the_prompt:
@@ -108,24 +110,26 @@ Blocking the prompt
backend_bases.FigureCanvasBase.stop_event_loop
-The simplest "integration" is to start the GUI event loop in
-"blocking" mode and take over the CLI. While the GUI event loop is
-running you cannot enter new commands into the prompt (your terminal
-may echo the characters typed into the terminal, but they will not be
-sent to the Python interpreter because it is busy running the GUI
-event loop), but the figure windows will be responsive. Once the
-event loop is stopped (leaving any still open figure windows
-non-responsive) you will be able to use the prompt again. Re-starting
-the event loop will make any open figure responsive again (and will
-process any queued up user interaction).
+The simplest solution is to start the GUI event loop and let it run
+exclusively, which results in responsive figure windows. However, the
+CLI event loop will not run, so that you cannot enter new commands.
+We call this "blocking" mode. (Your terminal may echo the typed characters,
+but they will not yet be processed by the CLI event loop because the Python
+interpreter is busy running the GUI event loop).
+
+It is possible to stop the GUI event loop and return control to the CLI
+event loop. You can then use the prompt again, but any still open figure
+windows are non-responsive. Re-starting the GUI event loop will make these
+figure responsive again (and will process any queued up user interaction).
+
-To start the event loop until all open figures are closed, use
-`.pyplot.show` as ::
+The typical command to show all figures and run the GUI event loop
+exclusively until all figures are closed is ::
- pyplot.show(block=True)
+ plt.show()
-To start the event loop for a fixed amount of time (in seconds) use
-`.pyplot.pause`.
+Alternatively, you can start the GUI event loop for a fixed amount of time
+using `.pyplot.pause`.
If you are not using `.pyplot` you can start and stop the event loops
via `.FigureCanvasBase.start_event_loop` and
From 479ed1e366967179e761d65e8020904efdbd2b43 Mon Sep 17 00:00:00 2001
From: Jody Klymak
Date: Sun, 30 Mar 2025 03:53:32 -0700
Subject: [PATCH 22/53] Backport PR #29834: TST: pin flake8
---
environment.yml | 2 +-
requirements/testing/flake8.txt | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/environment.yml b/environment.yml
index 8c4221d0a0fa..d95cab0509ff 100644
--- a/environment.yml
+++ b/environment.yml
@@ -53,7 +53,7 @@ dependencies:
# testing
- black<24
- coverage
- - flake8>=3.8
+ - flake8>=3.8,<7.2
- flake8-docstrings>=1.4.0
- gtk4
- ipykernel
diff --git a/requirements/testing/flake8.txt b/requirements/testing/flake8.txt
index a4d006b8551e..cb34fee3f385 100644
--- a/requirements/testing/flake8.txt
+++ b/requirements/testing/flake8.txt
@@ -1,6 +1,6 @@
# Extra pip requirements for the GitHub Actions flake8 build
-flake8>=3.8
+flake8>=3.8,<7.2
# versions less than 5.1.0 raise on some interp'd docstrings
pydocstyle>=5.1.0
# 1.4.0 adds docstring-convention=all
From d8654f512f8ebdaa3ba67b94c7b678889108f995 Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Mon, 7 Apr 2025 02:18:46 +0200
Subject: [PATCH 23/53] Backport PR #29853: Update lib/matplotlib/stackplot.py
---
lib/matplotlib/stackplot.py | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/lib/matplotlib/stackplot.py b/lib/matplotlib/stackplot.py
index 43da57c25da5..bd11558b0da9 100644
--- a/lib/matplotlib/stackplot.py
+++ b/lib/matplotlib/stackplot.py
@@ -26,11 +26,11 @@ def stackplot(axes, x, *args,
x : (N,) array-like
y : (M, N) array-like
- The data is assumed to be unstacked. Each of the following
+ The data can be either stacked or unstacked. Each of the following
calls is legal::
- stackplot(x, y) # where y has shape (M, N)
- stackplot(x, y1, y2, y3) # where y1, y2, y3, y4 have length N
+ stackplot(x, y) # where y has shape (M, N) e.g. y = [y1, y2, y3, y4]
+ stackplot(x, y1, y2, y3, y4) # where y1, y2, y3, y4 have length N
baseline : {'zero', 'sym', 'wiggle', 'weighted_wiggle'}
Method used to calculate the baseline:
From 877ec2966da2731e014913c142177e54db288444 Mon Sep 17 00:00:00 2001
From: Elliott Sales de Andrade
Date: Tue, 8 Apr 2025 20:08:27 -0400
Subject: [PATCH 24/53] Backport PR #29803: DOC: Improve FancyArrowPatch
docstring
---
lib/matplotlib/patches.py | 87 ++++++++++++++++++++++++++++-----------
1 file changed, 64 insertions(+), 23 deletions(-)
diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py
index f47c8abee32d..736b1e77e1ac 100644
--- a/lib/matplotlib/patches.py
+++ b/lib/matplotlib/patches.py
@@ -2335,7 +2335,7 @@ def get_styles(cls):
@classmethod
def pprint_styles(cls):
"""Return the available styles as pretty-printed string."""
- table = [('Class', 'Name', 'Attrs'),
+ table = [('Class', 'Name', 'Parameters'),
*[(cls.__name__,
# Add backquotes, as - and | have special meaning in reST.
f'``{name}``',
@@ -4159,49 +4159,90 @@ def __init__(self, posA=None, posB=None, *,
patchA=None, patchB=None, shrinkA=2, shrinkB=2,
mutation_scale=1, mutation_aspect=1, **kwargs):
"""
- There are two ways for defining an arrow:
+ **Defining the arrow position and path**
- - If *posA* and *posB* are given, a path connecting two points is
- created according to *connectionstyle*. The path will be
- clipped with *patchA* and *patchB* and further shrunken by
- *shrinkA* and *shrinkB*. An arrow is drawn along this
- resulting path using the *arrowstyle* parameter.
+ There are two ways to define the arrow position and path:
- - Alternatively if *path* is provided, an arrow is drawn along this
- path and *patchA*, *patchB*, *shrinkA*, and *shrinkB* are ignored.
+ - **Start, end and connection**:
+ The typical approach is to define the start and end points of the
+ arrow using *posA* and *posB*. The curve between these two can
+ further be configured using *connectionstyle*.
+
+ If given, the arrow curve is clipped by *patchA* and *patchB*,
+ allowing it to start/end at the border of these patches.
+ Additionally, the arrow curve can be shortened by *shrinkA* and *shrinkB*
+ to create a margin between start/end (after possible clipping) and the
+ drawn arrow.
+
+ - **path**: Alternatively if *path* is provided, an arrow is drawn along
+ this Path. In this case, *connectionstyle*, *patchA*, *patchB*,
+ *shrinkA*, and *shrinkB* are ignored.
+
+ **Styling**
+
+ The *arrowstyle* defines the styling of the arrow head, tail and shaft.
+ The resulting arrows can be styled further by setting the `.Patch`
+ properties such as *linewidth*, *color*, *facecolor*, *edgecolor*
+ etc. via keyword arguments.
Parameters
----------
- posA, posB : (float, float), default: None
- (x, y) coordinates of arrow tail and arrow head respectively.
+ posA, posB : (float, float), optional
+ (x, y) coordinates of start and end point of the arrow.
+ The actually drawn start and end positions may be modified
+ through *patchA*, *patchB*, *shrinkA*, and *shrinkB*.
- path : `~matplotlib.path.Path`, default: None
+ *posA*, *posB* are exclusive of *path*.
+
+ path : `~matplotlib.path.Path`, optional
If provided, an arrow is drawn along this path and *patchA*,
*patchB*, *shrinkA*, and *shrinkB* are ignored.
+ *path* is exclusive of *posA*, *posB*.
+
arrowstyle : str or `.ArrowStyle`, default: 'simple'
- The `.ArrowStyle` with which the fancy arrow is drawn. If a
- string, it should be one of the available arrowstyle names, with
- optional comma-separated attributes. The optional attributes are
- meant to be scaled with the *mutation_scale*. The following arrow
- styles are available:
+ The styling of arrow head, tail and shaft. This can be
+
+ - `.ArrowStyle` or one of its subclasses
+ - The shorthand string name (e.g. "->") as given in the table below,
+ optionally containing a comma-separated list of style parameters,
+ e.g. "->, head_length=10, head_width=5".
+
+ The style parameters are scaled by *mutation_scale*.
+
+ The following arrow styles are available. See also
+ :doc:`/gallery/text_labels_and_annotations/fancyarrow_demo`.
%(ArrowStyle:table)s
+ Only the styles ``<|-``, ``-|>``, ``<|-|>`` ``simple``, ``fancy``
+ and ``wedge`` contain closed paths and can be filled.
+
connectionstyle : str or `.ConnectionStyle` or None, optional, \
default: 'arc3'
- The `.ConnectionStyle` with which *posA* and *posB* are connected.
- If a string, it should be one of the available connectionstyle
- names, with optional comma-separated attributes. The following
- connection styles are available:
+ `.ConnectionStyle` with which *posA* and *posB* are connected.
+ This can be
+
+ - `.ConnectionStyle` or one of its subclasses
+ - The shorthand string name as given in the table below, e.g. "arc3".
%(ConnectionStyle:table)s
+ Ignored if *path* is provided.
+
patchA, patchB : `~matplotlib.patches.Patch`, default: None
- Head and tail patches, respectively.
+ Optional Patches at *posA* and *posB*, respectively. If given,
+ the arrow path is clipped by these patches such that head and tail
+ are at the border of the patches.
+
+ Ignored if *path* is provided.
shrinkA, shrinkB : float, default: 2
- Shrink amount, in points, of the tail and head of the arrow respectively.
+ Shorten the arrow path at *posA* and *posB* by this amount in points.
+ This allows to add a margin between the intended start/end points and
+ the arrow.
+
+ Ignored if *path* is provided.
mutation_scale : float, default: 1
Value with which attributes of *arrowstyle* (e.g., *head_length*)
From 7470a819a216db5dbb4de2eb2bb604b7a4b3fd03 Mon Sep 17 00:00:00 2001
From: hannah
Date: Thu, 10 Apr 2025 14:59:09 -0400
Subject: [PATCH 25/53] Backport PR #29896: Change `.T` to `.transpose()` in
`_reshape_2D`
---
lib/matplotlib/cbook.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py
index bb2ea311430e..b40f6549c945 100644
--- a/lib/matplotlib/cbook.py
+++ b/lib/matplotlib/cbook.py
@@ -1387,7 +1387,7 @@ def _reshape_2D(X, name):
# Iterate over columns for ndarrays.
if isinstance(X, np.ndarray):
- X = X.T
+ X = X.transpose()
if len(X) == 0:
return [[]]
From 02c05480af16fdb54e9f6d660721ad6c4937e71d Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Thu, 10 Apr 2025 23:23:16 +0200
Subject: [PATCH 26/53] Backport PR #29899: [doc] minimally document what basic
units is doing
---
galleries/examples/units/basic_units.py | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/galleries/examples/units/basic_units.py b/galleries/examples/units/basic_units.py
index 486918a8eafc..f7bdcc18b0dc 100644
--- a/galleries/examples/units/basic_units.py
+++ b/galleries/examples/units/basic_units.py
@@ -5,6 +5,14 @@
Basic units
===========
+
+This file implements a units library that supports registering arbitrary units,
+conversions between units, and math with unitized data. This library also implements a
+Matplotlib unit converter and registers its units with Matplotlib. This library is used
+in the examples to demonstrate Matplotlib's unit support. It is only maintained for the
+purposes of building documentation and should never be used outside of the Matplotlib
+documentation.
+
"""
import itertools
From 25b9377bec9587a3eca26518f57c31d647db2634 Mon Sep 17 00:00:00 2001
From: Elliott Sales de Andrade
Date: Wed, 16 Apr 2025 05:38:59 -0400
Subject: [PATCH 27/53] Backport PR #29897: BUG: ensure that errorbar does not
error on masked negative errors.
---
lib/matplotlib/axes/_axes.py | 9 ++++++---
lib/matplotlib/tests/test_axes.py | 19 ++++++++++++++++---
2 files changed, 22 insertions(+), 6 deletions(-)
diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py
index d11f916f7429..91dcf5a58acf 100644
--- a/lib/matplotlib/axes/_axes.py
+++ b/lib/matplotlib/axes/_axes.py
@@ -3813,9 +3813,12 @@ def apply_mask(arrays, mask):
f"'{dep_axis}err' must not contain None. "
"Use NaN if you want to skip a value.")
- res = np.zeros(err.shape, dtype=bool) # Default in case of nan
- if np.any(np.less(err, -err, out=res, where=(err == err))):
- # like err<0, but also works for timedelta and nan.
+ # Raise if any errors are negative, but not if they are nan.
+ # To avoid nan comparisons (which lead to warnings on some
+ # platforms), we select with `err==err` (which is False for nan).
+ # Also, since datetime.timedelta cannot be compared with 0,
+ # we compare with the negative error instead.
+ if np.any((check := err[err == err]) < -check):
raise ValueError(
f"'{dep_axis}err' must not contain negative values")
# This is like
diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py
index 952ccf3a3636..dbae45f203fa 100644
--- a/lib/matplotlib/tests/test_axes.py
+++ b/lib/matplotlib/tests/test_axes.py
@@ -4438,10 +4438,23 @@ def test_errorbar_nan(fig_test, fig_ref):
xs = range(5)
ys = np.array([1, 2, np.nan, np.nan, 3])
es = np.array([4, 5, np.nan, np.nan, 6])
- ax.errorbar(xs, ys, es)
+ ax.errorbar(xs, ys, yerr=es)
ax = fig_ref.add_subplot()
- ax.errorbar([0, 1], [1, 2], [4, 5])
- ax.errorbar([4], [3], [6], fmt="C0")
+ ax.errorbar([0, 1], [1, 2], yerr=[4, 5])
+ ax.errorbar([4], [3], yerr=[6], fmt="C0")
+
+
+@check_figures_equal()
+def test_errorbar_masked_negative(fig_test, fig_ref):
+ ax = fig_test.add_subplot()
+ xs = range(5)
+ mask = np.array([False, False, True, True, False])
+ ys = np.ma.array([1, 2, 2, 2, 3], mask=mask)
+ es = np.ma.array([4, 5, -1, -10, 6], mask=mask)
+ ax.errorbar(xs, ys, yerr=es)
+ ax = fig_ref.add_subplot()
+ ax.errorbar([0, 1], [1, 2], yerr=[4, 5])
+ ax.errorbar([4], [3], yerr=[6], fmt="C0")
@image_comparison(['hist_stacked_stepfilled', 'hist_stacked_stepfilled'])
From 88bd99c9991b76a6dba7b2cb67ce376909514c71 Mon Sep 17 00:00:00 2001
From: Oscar Gustafsson
Date: Wed, 16 Apr 2025 22:06:55 +0200
Subject: [PATCH 28/53] Backport PR #29929: Correct rightparen typo
---
lib/matplotlib/_mathtext.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py
index 6a1d9add9e8a..7085a986414e 100644
--- a/lib/matplotlib/_mathtext.py
+++ b/lib/matplotlib/_mathtext.py
@@ -521,7 +521,7 @@ def _get_glyph(self, fontname: str, font_class: str,
}
for alias, target in [(r'\leftparen', '('),
- (r'\rightparent', ')'),
+ (r'\rightparen', ')'),
(r'\leftbrace', '{'),
(r'\rightbrace', '}'),
(r'\leftbracket', '['),
From 54d66152e0a28ff76519ad0cdf4902be39338c11 Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Thu, 17 Apr 2025 07:34:14 +0200
Subject: [PATCH 29/53] Backport PR #29920: Allow `None` in set_prop_cycle (in
type hints)
---
lib/matplotlib/axes/_base.py | 2 +-
lib/matplotlib/axes/_base.pyi | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py
index 61d67d2791b4..bfee2091a1a6 100644
--- a/lib/matplotlib/axes/_base.py
+++ b/lib/matplotlib/axes/_base.py
@@ -1612,7 +1612,7 @@ def set_prop_cycle(self, *args, **kwargs):
Parameters
----------
- cycler : `~cycler.Cycler`
+ cycler : `~cycler.Cycler` or ``None``
Set the given Cycler. *None* resets to the cycle defined by the
current style.
diff --git a/lib/matplotlib/axes/_base.pyi b/lib/matplotlib/axes/_base.pyi
index ee3c7cf0dee9..df82ad7ec4a5 100644
--- a/lib/matplotlib/axes/_base.pyi
+++ b/lib/matplotlib/axes/_base.pyi
@@ -181,7 +181,7 @@ class _AxesBase(martist.Artist):
def get_facecolor(self) -> ColorType: ...
def set_facecolor(self, color: ColorType | None) -> None: ...
@overload
- def set_prop_cycle(self, cycler: Cycler) -> None: ...
+ def set_prop_cycle(self, cycler: Cycler | None) -> None: ...
@overload
def set_prop_cycle(self, label: str, values: Iterable[Any]) -> None: ...
@overload
From 9a5724130c447030ef7d91368339496147a5707c Mon Sep 17 00:00:00 2001
From: Kyle Sunden
Date: Thu, 17 Apr 2025 13:42:35 -0500
Subject: [PATCH 30/53] Backport PR #29931: Allow Python native sequences in
Matplotlib `imsave()`.
---
lib/matplotlib/image.py | 7 ++++++-
lib/matplotlib/tests/test_image.py | 21 +++++++++++++++++++++
2 files changed, 27 insertions(+), 1 deletion(-)
diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py
index 1cbd3c14c135..8191c5db0b2e 100644
--- a/lib/matplotlib/image.py
+++ b/lib/matplotlib/image.py
@@ -1536,7 +1536,8 @@ def imsave(fname, arr, vmin=None, vmax=None, cmap=None, format=None,
extension of *fname*, if any, and from :rc:`savefig.format` otherwise.
If *format* is set, it determines the output format.
arr : array-like
- The image data. The shape can be one of
+ The image data. Accepts NumPy arrays or sequences
+ (e.g., lists or tuples). The shape can be one of
MxN (luminance), MxNx3 (RGB) or MxNx4 (RGBA).
vmin, vmax : float, optional
*vmin* and *vmax* set the color scaling for the image by fixing the
@@ -1567,6 +1568,10 @@ def imsave(fname, arr, vmin=None, vmax=None, cmap=None, format=None,
default 'Software' key.
"""
from matplotlib.figure import Figure
+
+ # Normalizing input (e.g., list or tuples) to NumPy array if needed
+ arr = np.asanyarray(arr)
+
if isinstance(fname, os.PathLike):
fname = os.fspath(fname)
if format is None:
diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py
index bc98bdb3900c..ed94f154bbd6 100644
--- a/lib/matplotlib/tests/test_image.py
+++ b/lib/matplotlib/tests/test_image.py
@@ -184,6 +184,27 @@ def test_imsave(fmt):
assert_array_equal(arr_dpi1, arr_dpi100)
+def test_imsave_python_sequences():
+ # Tests saving an image with data passed using Python sequence types
+ # such as lists or tuples.
+
+ # RGB image: 3 rows × 2 columns, with float values in [0.0, 1.0]
+ img_data = [
+ [(1.0, 0.0, 0.0), (0.0, 1.0, 0.0)],
+ [(0.0, 0.0, 1.0), (1.0, 1.0, 0.0)],
+ [(0.0, 1.0, 1.0), (1.0, 0.0, 1.0)],
+ ]
+
+ buff = io.BytesIO()
+ plt.imsave(buff, img_data, format="png")
+ buff.seek(0)
+ read_img = plt.imread(buff)
+
+ assert_array_equal(
+ np.array(img_data),
+ read_img[:, :, :3] # Drop alpha if present
+ )
+
@pytest.mark.parametrize("origin", ["upper", "lower"])
def test_imsave_rgba_origin(origin):
# test that imsave always passes c-contiguous arrays down to pillow
From bdba952a62b51a6fa49e873043f1f9e1fc007340 Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Fri, 18 Apr 2025 12:25:31 +0200
Subject: [PATCH 31/53] Backport PR #29919: Handle MOVETO's, CLOSEPOLY's and
empty paths in Path.interpolated
---
lib/matplotlib/path.py | 29 +++++++++--
lib/matplotlib/tests/test_path.py | 81 +++++++++++++++++++++++++++++++
2 files changed, 106 insertions(+), 4 deletions(-)
diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py
index 5f5a0f3de423..3fa3fb262e27 100644
--- a/lib/matplotlib/path.py
+++ b/lib/matplotlib/path.py
@@ -668,14 +668,35 @@ def intersects_bbox(self, bbox, filled=True):
def interpolated(self, steps):
"""
- Return a new path resampled to length N x *steps*.
+ Return a new path with each segment divided into *steps* parts.
- Codes other than `LINETO` are not handled correctly.
+ Codes other than `LINETO`, `MOVETO`, and `CLOSEPOLY` are not handled correctly.
+
+ Parameters
+ ----------
+ steps : int
+ The number of segments in the new path for each in the original.
+
+ Returns
+ -------
+ Path
+ The interpolated path.
"""
- if steps == 1:
+ if steps == 1 or len(self) == 0:
return self
- vertices = simple_linear_interpolation(self.vertices, steps)
+ if self.codes is not None and self.MOVETO in self.codes[1:]:
+ return self.make_compound_path(
+ *(p.interpolated(steps) for p in self._iter_connected_components()))
+
+ if self.codes is not None and self.CLOSEPOLY in self.codes and not np.all(
+ self.vertices[self.codes == self.CLOSEPOLY] == self.vertices[0]):
+ vertices = self.vertices.copy()
+ vertices[self.codes == self.CLOSEPOLY] = vertices[0]
+ else:
+ vertices = self.vertices
+
+ vertices = simple_linear_interpolation(vertices, steps)
codes = self.codes
if codes is not None:
new_codes = np.full((len(codes) - 1) * steps + 1, Path.LINETO,
diff --git a/lib/matplotlib/tests/test_path.py b/lib/matplotlib/tests/test_path.py
index 5424160dad93..21f4c33794af 100644
--- a/lib/matplotlib/tests/test_path.py
+++ b/lib/matplotlib/tests/test_path.py
@@ -541,3 +541,84 @@ def test_cleanup_closepoly():
cleaned = p.cleaned(remove_nans=True)
assert len(cleaned) == 1
assert cleaned.codes[0] == Path.STOP
+
+
+def test_interpolated_moveto():
+ # Initial path has two subpaths with two LINETOs each
+ vertices = np.array([[0, 0],
+ [0, 1],
+ [1, 2],
+ [4, 4],
+ [4, 5],
+ [5, 5]])
+ codes = [Path.MOVETO, Path.LINETO, Path.LINETO] * 2
+
+ path = Path(vertices, codes)
+ result = path.interpolated(3)
+
+ # Result should have two subpaths with six LINETOs each
+ expected_subpath_codes = [Path.MOVETO] + [Path.LINETO] * 6
+ np.testing.assert_array_equal(result.codes, expected_subpath_codes * 2)
+
+
+def test_interpolated_closepoly():
+ codes = [Path.MOVETO] + [Path.LINETO]*2 + [Path.CLOSEPOLY]
+ vertices = [(4, 3), (5, 4), (5, 3), (0, 0)]
+
+ path = Path(vertices, codes)
+ result = path.interpolated(2)
+
+ expected_vertices = np.array([[4, 3],
+ [4.5, 3.5],
+ [5, 4],
+ [5, 3.5],
+ [5, 3],
+ [4.5, 3],
+ [4, 3]])
+ expected_codes = [Path.MOVETO] + [Path.LINETO]*5 + [Path.CLOSEPOLY]
+
+ np.testing.assert_allclose(result.vertices, expected_vertices)
+ np.testing.assert_array_equal(result.codes, expected_codes)
+
+ # Usually closepoly is the last vertex but does not have to be.
+ codes += [Path.LINETO]
+ vertices += [(2, 1)]
+
+ path = Path(vertices, codes)
+ result = path.interpolated(2)
+
+ extra_expected_vertices = np.array([[3, 2],
+ [2, 1]])
+ expected_vertices = np.concatenate([expected_vertices, extra_expected_vertices])
+
+ expected_codes += [Path.LINETO] * 2
+
+ np.testing.assert_allclose(result.vertices, expected_vertices)
+ np.testing.assert_array_equal(result.codes, expected_codes)
+
+
+def test_interpolated_moveto_closepoly():
+ # Initial path has two closed subpaths
+ codes = ([Path.MOVETO] + [Path.LINETO]*2 + [Path.CLOSEPOLY]) * 2
+ vertices = [(4, 3), (5, 4), (5, 3), (0, 0), (8, 6), (10, 8), (10, 6), (0, 0)]
+
+ path = Path(vertices, codes)
+ result = path.interpolated(2)
+
+ expected_vertices1 = np.array([[4, 3],
+ [4.5, 3.5],
+ [5, 4],
+ [5, 3.5],
+ [5, 3],
+ [4.5, 3],
+ [4, 3]])
+ expected_vertices = np.concatenate([expected_vertices1, expected_vertices1 * 2])
+ expected_codes = ([Path.MOVETO] + [Path.LINETO]*5 + [Path.CLOSEPOLY]) * 2
+
+ np.testing.assert_allclose(result.vertices, expected_vertices)
+ np.testing.assert_array_equal(result.codes, expected_codes)
+
+
+def test_interpolated_empty_path():
+ path = Path(np.zeros((0, 2)))
+ assert path.interpolated(42) is path
From a1440fe34e39f34209e22fa0d6289134ef12c006 Mon Sep 17 00:00:00 2001
From: David Stansby
Date: Fri, 18 Apr 2025 12:52:19 +0100
Subject: [PATCH 32/53] Fix broken links in interactive guide
---
galleries/users_explain/figure/interactive_guide.rst | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/galleries/users_explain/figure/interactive_guide.rst b/galleries/users_explain/figure/interactive_guide.rst
index 6cbdca4ab3f9..21658bb5849b 100644
--- a/galleries/users_explain/figure/interactive_guide.rst
+++ b/galleries/users_explain/figure/interactive_guide.rst
@@ -324,7 +324,7 @@ with what is displayed on the screen. This is intended to be used to
determine if ``draw_idle`` should be called to schedule a re-rendering
of the figure.
-Each artist has a `.Artist.stale_callback` attribute which holds a callback
+Each artist has a `!Artist.stale_callback` attribute which holds a callback
with the signature ::
def callback(self: Artist, val: bool) -> None:
@@ -343,7 +343,7 @@ default callback is `None`. If you call `.pyplot.ion` and are not in
`~.backend_bases.FigureCanvasBase.draw_idle` on any stale figures
after having executed the user's input, but before returning the prompt
to the user. If you are not using `.pyplot` you can use the callback
-`Figure.stale_callback` attribute to be notified when a figure has
+`!Figure.stale_callback` attribute to be notified when a figure has
become stale.
@@ -424,7 +424,7 @@ IPython / prompt_toolkit
With IPython >= 5.0 IPython has changed from using CPython's readline
based prompt to a ``prompt_toolkit`` based prompt. ``prompt_toolkit``
has the same conceptual input hook, which is fed into ``prompt_toolkit`` via the
-:meth:`IPython.terminal.interactiveshell.TerminalInteractiveShell.inputhook`
+:meth:`!IPython.terminal.interactiveshell.TerminalInteractiveShell.inputhook`
method. The source for the ``prompt_toolkit`` input hooks lives at
``IPython.terminal.pt_inputhooks``.
From 4cd3b06a78a4db8ef0453844e470ae2ef495381b Mon Sep 17 00:00:00 2001
From: Thomas A Caswell
Date: Fri, 18 Apr 2025 10:46:56 -0400
Subject: [PATCH 33/53] STY: placate lint
---
lib/matplotlib/tests/test_image.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py
index ed94f154bbd6..40dc6b812186 100644
--- a/lib/matplotlib/tests/test_image.py
+++ b/lib/matplotlib/tests/test_image.py
@@ -205,6 +205,7 @@ def test_imsave_python_sequences():
read_img[:, :, :3] # Drop alpha if present
)
+
@pytest.mark.parametrize("origin", ["upper", "lower"])
def test_imsave_rgba_origin(origin):
# test that imsave always passes c-contiguous arrays down to pillow
From d1c20eac85233fb0f3456333a72fea728c5936a8 Mon Sep 17 00:00:00 2001
From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com>
Date: Thu, 10 Apr 2025 23:17:56 +0200
Subject: [PATCH 34/53] Backport PR #29872: TST: Use placeholders for text in
layout tests
---
doc/devel/testing.rst | 7 +-
lib/matplotlib/testing/conftest.py | 51 +
lib/matplotlib/testing/conftest.pyi | 2 +
.../constrained_layout1.png | Bin 23698 -> 5377 bytes
.../constrained_layout10.png | Bin 36013 -> 16820 bytes
.../constrained_layout11.png | Bin 37569 -> 16765 bytes
.../constrained_layout11rat.png | Bin 34457 -> 13931 bytes
.../constrained_layout12.png | Bin 40909 -> 12369 bytes
.../constrained_layout13.png | Bin 31949 -> 3853 bytes
.../constrained_layout14.png | Bin 32807 -> 6538 bytes
.../constrained_layout15.png | Bin 27748 -> 6503 bytes
.../constrained_layout16.png | Bin 23123 -> 4728 bytes
.../constrained_layout17.png | Bin 34175 -> 8786 bytes
.../constrained_layout2.png | Bin 34454 -> 6274 bytes
.../constrained_layout3.png | Bin 41207 -> 7888 bytes
.../constrained_layout4.png | Bin 36274 -> 5577 bytes
.../constrained_layout5.png | Bin 34021 -> 5031 bytes
.../constrained_layout6.png | Bin 39012 -> 10865 bytes
.../constrained_layout8.png | Bin 31057 -> 6995 bytes
.../constrained_layout9.png | Bin 30088 -> 5705 bytes
.../test_colorbars_no_overlapH.png | Bin 4519 -> 2050 bytes
.../test_colorbars_no_overlapV.png | Bin 7189 -> 3548 bytes
.../test_table/table_cell_manipulation.png | Bin 7953 -> 1495 bytes
.../test_tightlayout/tight_layout1.pdf | Bin 7097 -> 1622 bytes
.../test_tightlayout/tight_layout1.png | Bin 23037 -> 4172 bytes
.../test_tightlayout/tight_layout1.svg | 557 ++----
.../test_tightlayout/tight_layout2.pdf | Bin 7813 -> 2390 bytes
.../test_tightlayout/tight_layout2.png | Bin 27230 -> 6476 bytes
.../test_tightlayout/tight_layout2.svg | 1322 +++++---------
.../test_tightlayout/tight_layout3.pdf | Bin 7661 -> 2162 bytes
.../test_tightlayout/tight_layout3.png | Bin 34090 -> 8235 bytes
.../test_tightlayout/tight_layout3.svg | 1058 ++++-------
.../test_tightlayout/tight_layout4.pdf | Bin 7864 -> 2440 bytes
.../test_tightlayout/tight_layout4.png | Bin 33327 -> 8171 bytes
.../test_tightlayout/tight_layout4.svg | 1322 +++++---------
.../test_tightlayout/tight_layout5.pdf | Bin 5956 -> 2334 bytes
.../test_tightlayout/tight_layout5.png | Bin 8519 -> 2271 bytes
.../test_tightlayout/tight_layout5.svg | 431 ++---
.../test_tightlayout/tight_layout6.pdf | Bin 8083 -> 2708 bytes
.../test_tightlayout/tight_layout6.png | Bin 46009 -> 8776 bytes
.../test_tightlayout/tight_layout6.svg | 1483 ++++++---------
.../test_tightlayout/tight_layout7.pdf | Bin 8692 -> 1632 bytes
.../test_tightlayout/tight_layout7.png | Bin 27049 -> 4246 bytes
.../test_tightlayout/tight_layout7.svg | 692 ++-----
.../test_tightlayout/tight_layout8.pdf | Bin 7213 -> 1594 bytes
.../test_tightlayout/tight_layout8.png | Bin 23737 -> 5359 bytes
.../test_tightlayout/tight_layout8.svg | 557 ++----
.../test_tightlayout/tight_layout9.pdf | Bin 6066 -> 2215 bytes
.../test_tightlayout/tight_layout9.png | Bin 18802 -> 1534 bytes
.../test_tightlayout/tight_layout9.svg | 1181 +++++-------
.../tight_layout_offsetboxes1.pdf | Bin 7162 -> 0 bytes
.../tight_layout_offsetboxes1.png | Bin 31886 -> 0 bytes
.../tight_layout_offsetboxes1.svg | 1613 -----------------
.../tight_layout_offsetboxes2.pdf | Bin 6954 -> 0 bytes
.../tight_layout_offsetboxes2.png | Bin 36237 -> 0 bytes
.../tight_layout_offsetboxes2.svg | 1469 ---------------
lib/matplotlib/tests/conftest.py | 2 +-
.../tests/test_constrainedlayout.py | 56 +-
lib/matplotlib/tests/test_table.py | 7 +-
lib/matplotlib/tests/test_tightlayout.py | 107 +-
.../test_axislines/axisline_style_tight.png | Bin 1876 -> 999 bytes
.../axisartist/tests/test_axislines.py | 4 +-
62 files changed, 2809 insertions(+), 9112 deletions(-)
delete mode 100644 lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout_offsetboxes1.pdf
delete mode 100644 lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout_offsetboxes1.png
delete mode 100644 lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout_offsetboxes1.svg
delete mode 100644 lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout_offsetboxes2.pdf
delete mode 100644 lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout_offsetboxes2.png
delete mode 100644 lib/matplotlib/tests/baseline_images/test_tightlayout/tight_layout_offsetboxes2.svg
diff --git a/doc/devel/testing.rst b/doc/devel/testing.rst
index 72f787eca746..97e907a0fbca 100644
--- a/doc/devel/testing.rst
+++ b/doc/devel/testing.rst
@@ -161,9 +161,10 @@ the tests, they should now pass.
It is preferred that new tests use ``style='mpl20'`` as this leads to smaller
figures and reflects the newer look of default Matplotlib plots. Also, if the
-texts (labels, tick labels, etc) are not really part of what is tested, use
-``remove_text=True`` as this will lead to smaller figures and reduce possible
-issues with font mismatch on different platforms.
+texts (labels, tick labels, etc) are not really part of what is tested, use the
+``remove_text=True`` argument or add the ``text_placeholders`` fixture as this
+will lead to smaller figures and reduce possible issues with font mismatch on
+different platforms.
Compare two methods of creating an image
diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py
index 3f96de611195..2961e7f02f3f 100644
--- a/lib/matplotlib/testing/conftest.py
+++ b/lib/matplotlib/testing/conftest.py
@@ -125,3 +125,54 @@ def test_imshow_xarray(xr):
xr = pytest.importorskip('xarray')
return xr
+
+
+@pytest.fixture
+def text_placeholders(monkeypatch):
+ """
+ Replace texts with placeholder rectangles.
+
+ The rectangle size only depends on the font size and the number of characters. It is
+ thus insensitive to font properties and rendering details. This should be used for
+ tests that depend on text geometries but not the actual text rendering, e.g. layout
+ tests.
+ """
+ from matplotlib.patches import Rectangle
+
+ def patched_get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi):
+ """
+ Replace ``_get_text_metrics_with_cache`` with fixed results.
+
+ The usual ``renderer.get_text_width_height_descent`` would depend on font
+ metrics; instead the fixed results are based on font size and the length of the
+ string only.
+ """
+ # While get_window_extent returns pixels and font size is in points, font size
+ # includes ascenders and descenders. Leaving out this factor and setting
+ # descent=0 ends up with a box that is relatively close to DejaVu Sans.
+ height = fontprop.get_size()
+ width = len(text) * height / 1.618 # Golden ratio for character size.
+ descent = 0
+ return width, height, descent
+
+ def patched_text_draw(self, renderer):
+ """
+ Replace ``Text.draw`` with a fixed bounding box Rectangle.
+
+ The bounding box corresponds to ``Text.get_window_extent``, which ultimately
+ depends on the above patched ``_get_text_metrics_with_cache``.
+ """
+ if renderer is not None:
+ self._renderer = renderer
+ if not self.get_visible():
+ return
+ if self.get_text() == '':
+ return
+ bbox = self.get_window_extent()
+ rect = Rectangle(bbox.p0, bbox.width, bbox.height,
+ facecolor=self.get_color(), edgecolor='none')
+ rect.draw(renderer)
+
+ monkeypatch.setattr('matplotlib.text._get_text_metrics_with_cache',
+ patched_get_text_metrics_with_cache)
+ monkeypatch.setattr('matplotlib.text.Text.draw', patched_text_draw)
diff --git a/lib/matplotlib/testing/conftest.pyi b/lib/matplotlib/testing/conftest.pyi
index 2af0eb93cc8a..f5d90bc88f73 100644
--- a/lib/matplotlib/testing/conftest.pyi
+++ b/lib/matplotlib/testing/conftest.pyi
@@ -10,3 +10,5 @@ def mpl_test_settings(request: pytest.FixtureRequest) -> None: ...
def pd() -> ModuleType: ...
@pytest.fixture
def xr() -> ModuleType: ...
+@pytest.fixture
+def text_placeholders(monkeypatch: pytest.MonkeyPatch) -> None: ...
diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout1.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout1.png
index 25eade2b6297dfc699f763eada42fa6b72928b67..bfbbfd6f7eebee4b72e1cc5f5e72aa3b3060c615 100644
GIT binary patch
literal 5377
zcmZ`-XH=72vrR&PsFa|-N>c;ANKqgtRX{_pB1lsZ2q0CO0wOg5=~YCEbb)|?H0eEb
z5F!|=^xiw6xBJ9zt^4QRFKcIIa@Ne*GpFo%k|1?81=@?u7eOEpt&*bbGY|-Z1%bem
zFbZGw`{MHQg#MsHf#@@-s$^`Mw?5(4fz1;&o0RcWF-z&3s
z!h#}qEksNOg@w(SY0ysT6Jv%-*Jvuo(JU%@*KG{Dy**iSh
zJv=@*I3OJy?;IR&?;maLA8qa(ZSEdz>>jR@4%bMBt2+lPI|o}^Tg%%Ao12?U+xtsf
z`-@xq3!D4%n|pH`d$SvRGwZw4Yr9jcyOXP=i51fL3TbS4XLNaIWC_6b@Z$EL#qFVm
z?ZJhunVFe^`K|uBt-iU<-q}s!%;xWzjh^X^?&*zRQ|pAO^{z<(Yn>BoKPT2Y#@E`%
zR@=r_TSr%0Mps%!R+>jvnub>zhnE}vEaU$y*AFe#0T^7W9bBv#Sgh_}tm|3bl
zTPW`ZFkeQTFD1^G{GKoVJ@=z$uBd0OuzR-P*KGc;nLNVG_pX`Tu9=+9>1+T$r@#H2
z%IcWP?3lu}Pi3@ErngO|0cf50+B%WiGLh0ep4>d1)HI&hG@j5n7T+-TrC~GW^>9S>pYWsR9eEUs`qI
zTD1Yhwra(;XvVZWi)qn_ZdQ+Oe)_pt4M0@Wlc*-uPfaSH8kHj(l|D8qer!~TXux1F
z^5G3~;dt3Fyi6EgI<#I2KuA3*r2cVm-J{?-$)Gxkpjz?3+J^zPV*WLv{xu?gHNt+?
zLcZ04zSR#t0I0g}QzhV2h4ilE_pao_R`Oyh?g8+sxa(EH<5|w_Son&~TBrB!$et30^5c1>=YulFN`sf$;
zf(GB9nP5xyNY?o6kCa7h!k3kTDKA}Pztr3)^W*Caw!3LpFDZRW;|ki23%IKJftkKn
zkkFeH(EXxo)xxXSX+w3w+-qZSVsqU|ZQ=O%37X!r
zID!=hzllJJ!VwZEtOEk|9t)a6VK+hGMsLs*7~)F-Zlw4>0p#g`S8K!%(Ock_jy5x?
zplK+VA?s(Zlx7C?M9~gvj^IyRpy2!fkO1R<;bNpZ21S8kaA;srIA}^5%gF!si^fF<
zldEz9U=z!`kCK>hpb5@X7+j_!aa~7-<0)$B`H_5;0YMz0ni262$)58TTVc_rpA+Vq
zQ3EpaVPEEaEnEFGI&D6P^#10#k1_k`VA{Y3Zhr=}$sWbhAtcin%|5zol%&ubYTD3D
z5+|;3bWt?64#+zv<)9J&+{t_y`)J@k11$>+OP@Pir{W`3)v9}*`b}TpL(h*5a&l+Y
zFRRY-?d5n;WKE+@=PZ`={%N~!%{qj_2Na|+jD_q+UM
zX(RXjQ!_Ky{5L1#QB3~eC4-E1gt7)hA^N(?s=~MLcy(<^#lJgU1e4wJr4>K
z)v@6Uei(d_=s(VcX3SiFeeOVKQTd6{yeZXvW5F|d|M-xiwRC;gAmU!dK#d(K;oq>8
z#`5V&+3|1AJdZgV+AdDXZd`b#L|+$kf%Ec$&bR4NXv%B9E0W3?7itd;k%@|l&+>CL
zTs;bz)ts0(pDcG@@11JBprY-l4Nh4NxUzP}7fmO88&%x04hO5D`rh5JH5
zQ;lb4#3TY>NEa6oSB=e~uflxG~wzvj`&D6`f*1k(xoh
zI#y#Xu6q;^Sv6-*rK5_1Qx-6LfIq(`VG(jn4-HJTIxiv3sqLk};9`!=(*0xVmXGDJ
z?O{6fDidqw=YF6_7V6L3uI`bxz;qOZjTrN_*DUO)S$uLY&Rzex3=IB9M=;ZHhpvc<
z0rA7O{pwPYa10fgJvaWPer`*f-a}~M_hGLeBo76Z2vDTtVXW@!mUds90=7N*(JjRS
z=i)GS)GMWN%GagyUB0$g!R#t!UYc&zL&5+vZO{Xq?CnO0k*g3{VJFWQ@fArz5HS0W
z{5{>>e+~y2p@FI^o1vtZ@BuEs6t|9C&gUlbc37Z^7&3c{Rb_b5Q|7yWLLt$iS9*C2UT3Iexv<$fep&6%3
zQR#kH7|4#t&z;xlbt*i6#eg0vJ5_XZz8k^^p?NXx7p>NZogr@V+16*}N!ebcnU%7G)p#{{=
z1u)gCL3^BnUV<(R$>@jq-rIPa@Bqpfe53wjvYmY$8y