diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml
index 41d28d864dbe..41f5bca65f18 100644
--- a/.github/workflows/cibuildwheel.yml
+++ b/.github/workflows/cibuildwheel.yml
@@ -105,6 +105,7 @@ jobs:
CIBW_SKIP: "*-musllinux_aarch64"
CIBW_TEST_COMMAND: >-
python {package}/ci/check_version_number.py
+ MACOSX_DEPLOYMENT_TARGET: "10.12"
MPL_DISABLE_FH4: "yes"
strategy:
matrix:
@@ -115,16 +116,10 @@ jobs:
cibw_archs: "aarch64"
- os: windows-latest
cibw_archs: "auto64"
- - os: macos-11
+ - os: macos-12
cibw_archs: "x86_64"
- # NOTE: macos_target can be moved back into global environment after
- # meson-python 0.16.0 is released.
- macos_target: "10.12"
- os: macos-14
cibw_archs: "arm64"
- # NOTE: macos_target can be moved back into global environment after
- # meson-python 0.16.0 is released.
- macos_target: "11.0"
steps:
- name: Set up QEMU
@@ -140,49 +135,44 @@ jobs:
path: dist/
- name: Build wheels for CPython 3.12
- uses: pypa/cibuildwheel@8d945475ac4b1aac4ae08b2fd27db9917158b6ce # v2.17.0
+ uses: pypa/cibuildwheel@ba8be0d98853f5744f24e7f902c8adef7ae2e7f3 # v2.18.1
with:
package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }}
env:
CIBW_BUILD: "cp312-*"
CIBW_ARCHS: ${{ matrix.cibw_archs }}
- MACOSX_DEPLOYMENT_TARGET: "${{ matrix.macos_target }}"
- name: Build wheels for CPython 3.11
- uses: pypa/cibuildwheel@8d945475ac4b1aac4ae08b2fd27db9917158b6ce # v2.17.0
+ uses: pypa/cibuildwheel@ba8be0d98853f5744f24e7f902c8adef7ae2e7f3 # v2.18.1
with:
package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }}
env:
CIBW_BUILD: "cp311-*"
CIBW_ARCHS: ${{ matrix.cibw_archs }}
- MACOSX_DEPLOYMENT_TARGET: "${{ matrix.macos_target }}"
- name: Build wheels for CPython 3.10
- uses: pypa/cibuildwheel@8d945475ac4b1aac4ae08b2fd27db9917158b6ce # v2.17.0
+ uses: pypa/cibuildwheel@ba8be0d98853f5744f24e7f902c8adef7ae2e7f3 # v2.18.1
with:
package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }}
env:
CIBW_BUILD: "cp310-*"
CIBW_ARCHS: ${{ matrix.cibw_archs }}
- MACOSX_DEPLOYMENT_TARGET: "${{ matrix.macos_target }}"
- name: Build wheels for CPython 3.9
- uses: pypa/cibuildwheel@8d945475ac4b1aac4ae08b2fd27db9917158b6ce # v2.17.0
+ uses: pypa/cibuildwheel@ba8be0d98853f5744f24e7f902c8adef7ae2e7f3 # v2.18.1
with:
package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }}
env:
CIBW_BUILD: "cp39-*"
CIBW_ARCHS: ${{ matrix.cibw_archs }}
- MACOSX_DEPLOYMENT_TARGET: "${{ matrix.macos_target }}"
- name: Build wheels for PyPy
- uses: pypa/cibuildwheel@8d945475ac4b1aac4ae08b2fd27db9917158b6ce # v2.17.0
+ uses: pypa/cibuildwheel@ba8be0d98853f5744f24e7f902c8adef7ae2e7f3 # v2.18.1
with:
package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }}
env:
CIBW_BUILD: "pp39-*"
CIBW_ARCHS: ${{ matrix.cibw_archs }}
- MACOSX_DEPLOYMENT_TARGET: "${{ matrix.macos_target }}"
if: matrix.cibw_archs != 'aarch64'
- uses: actions/upload-artifact@v4
@@ -199,6 +189,8 @@ jobs:
environment: release
permissions:
id-token: write
+ attestations: write
+ contents: read
steps:
- name: Download packages
uses: actions/download-artifact@v4
@@ -210,5 +202,10 @@ jobs:
- name: Print out packages
run: ls dist
+ - name: Generate artifact attestation for sdist and wheel
+ uses: actions/attest-build-provenance@49df96e17e918a15956db358890b08e61c704919 # v1.2.0
+ with:
+ subject-path: dist/matplotlib-*
+
- name: Publish package distributions to PyPI
uses: pypa/gh-action-pypi-publish@81e9d935c883d0b210363ab89cf05f3894778450 # v1.8.14
diff --git a/.github/workflows/circleci.yml b/.github/workflows/circleci.yml
index 8f9e3190c5e2..3aead720cf20 100644
--- a/.github/workflows/circleci.yml
+++ b/.github/workflows/circleci.yml
@@ -10,7 +10,8 @@ jobs:
name: Run CircleCI artifacts redirector
steps:
- name: GitHub Action step
- uses: larsoner/circleci-artifacts-redirector-action@master
+ uses:
+ scientific-python/circleci-artifacts-redirector-action@4e13a10d89177f4bfc8007a7064bdbeda848d8d1 # v1.0.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
api-token: ${{ secrets.CIRCLECI_TOKEN }}
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 29d2859999bd..203b0eee9ca4 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -32,7 +32,6 @@ jobs:
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
- setup-python-dependencies: false
- name: Build compiled code
if: matrix.language == 'c-cpp'
diff --git a/.github/workflows/conflictcheck.yml b/.github/workflows/conflictcheck.yml
index 3eb384fa6585..3110839e5150 100644
--- a/.github/workflows/conflictcheck.yml
+++ b/.github/workflows/conflictcheck.yml
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check if PRs have merge conflicts
- uses: eps1lon/actions-label-merge-conflict@e62d7a53ff8be8b97684bffb6cfbbf3fc1115e2e # v3.0.0
+ uses: eps1lon/actions-label-merge-conflict@1b1b1fcde06a9b3d089f3464c96417961dde1168 # v3.0.2
with:
dirtyLabel: "status: needs rebase"
repoToken: "${{ secrets.GITHUB_TOKEN }}"
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 126693beafa7..daa07e62b2e5 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -160,10 +160,9 @@ jobs:
fi
;;
macOS)
- brew install ccache
- brew tap homebrew/cask-fonts
- brew install font-noto-sans-cjk ghostscript gobject-introspection gtk4 ninja
- brew install --cask inkscape
+ brew update
+ brew install ccache ghostscript gobject-introspection gtk4 ninja
+ brew install --cask font-noto-sans-cjk inkscape
;;
esac
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2dc1ca5352c0..14817e95929f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -79,7 +79,7 @@ repos:
- id: yamllint
args: ["--strict", "--config-file=.yamllint.yml"]
- repo: https://github.com/python-jsonschema/check-jsonschema
- rev: 0.28.1
+ rev: 0.28.4
hooks:
# TODO: Re-enable this when https://github.com/microsoft/azure-pipelines-vscode/issues/567 is fixed.
# - id: check-azure-pipelines
diff --git a/.yamllint.yml b/.yamllint.yml
index 3b30533ececa..2be81b28c7fb 100644
--- a/.yamllint.yml
+++ b/.yamllint.yml
@@ -3,7 +3,7 @@ extends: default
rules:
line-length:
- max: 111
+ max: 120
allow-non-breakable-words: true
truthy:
check-keys: false
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index bf055d0eaa16..4c50c543846a 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -119,10 +119,10 @@ stages:
texlive-xetex
;;
Darwin)
+ brew update
brew install --cask xquartz
brew install ccache ffmpeg imagemagick mplayer ninja pkg-config
- brew tap homebrew/cask-fonts
- brew install font-noto-sans-cjk-sc
+ brew install --cask font-noto-sans-cjk-sc
;;
Windows_NT)
choco install ninja
diff --git a/doc/_static/switcher.json b/doc/_static/switcher.json
index 6996d79bc22a..3d712e4ff8e9 100644
--- a/doc/_static/switcher.json
+++ b/doc/_static/switcher.json
@@ -2,7 +2,8 @@
{
"name": "3.9 (stable)",
"version": "stable",
- "url": "https://matplotlib.org/stable/"
+ "url": "https://matplotlib.org/stable/",
+ "preferred": true
},
{
"name": "3.10 (dev)",
diff --git a/doc/api/prev_api_changes/api_changes_3.5.0/development.rst b/doc/api/prev_api_changes/api_changes_3.5.0/development.rst
index 2db21237a699..b42e6eff3423 100644
--- a/doc/api/prev_api_changes/api_changes_3.5.0/development.rst
+++ b/doc/api/prev_api_changes/api_changes_3.5.0/development.rst
@@ -77,6 +77,6 @@ In order to avoid conflicting with the use of :file:`setup.cfg` by
``setup.cfg`` to ``mplsetup.cfg``. The :file:`setup.cfg.template` has been
correspondingly been renamed to :file:`mplsetup.cfg.template`.
-Note that the path to this configuration file can still be set via the
-:envvar:`MPLSETUPCFG` environment variable, which allows one to keep using the
-same file before and after this change.
+Note that the path to this configuration file can still be set via the ``MPLSETUPCFG``
+environment variable, which allows one to keep using the same file before and after this
+change.
diff --git a/doc/conf.py b/doc/conf.py
index c9a475aecf9c..92d78f896ca2 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -508,6 +508,7 @@ def js_tag_with_cache_busting(js):
# this special value indicates the use of the unreleased banner. If we need
# an actual announcement, then just place the text here as usual.
"announcement": "unreleased" if not is_release_build else "",
+ "show_version_warning_banner": True,
}
include_analytics = is_release_build
if include_analytics:
diff --git a/doc/install/environment_variables_faq.rst b/doc/install/environment_variables_faq.rst
index ba384343cc5a..38e0d0ef0c63 100644
--- a/doc/install/environment_variables_faq.rst
+++ b/doc/install/environment_variables_faq.rst
@@ -29,13 +29,6 @@ Environment variables
used to find a base directory in which the :file:`matplotlib` subdirectory is
created.
-.. envvar:: MPLSETUPCFG
-
- This optional variable can be set to the full path of a :file:`mplsetup.cfg`
- configuration file used to customize the Matplotlib build. By default, a
- :file:`mplsetup.cfg` file in the root of the Matplotlib source tree will be
- read. Supported build options are listed in :file:`mplsetup.cfg.template`.
-
.. envvar:: PATH
The list of directories searched to find executable programs.
diff --git a/doc/install/index.rst b/doc/install/index.rst
index ea8e29d71565..867e4600a77e 100644
--- a/doc/install/index.rst
+++ b/doc/install/index.rst
@@ -121,22 +121,20 @@ Before trying to install Matplotlib, please install the :ref:`dependencies`.
To build from a tarball, download the latest *tar.gz* release
file from `the PyPI files page `_.
-We provide a `mplsetup.cfg`_ file which you can use to customize the build
-process. For example, which default backend to use, whether some of the
-optional libraries that Matplotlib ships with are installed, and so on. This
-file will be particularly useful to those packaging Matplotlib.
-
-.. _mplsetup.cfg: https://raw.githubusercontent.com/matplotlib/matplotlib/main/mplsetup.cfg.template
-
If you are building your own Matplotlib wheels (or sdists) on Windows, note
that any DLLs that you copy into the source tree will be packaged too.
-
Configure build and behavior defaults
=====================================
-Aspects of the build and install process and some behaviorial defaults of the
-library can be configured via:
+We provide a `meson.options`_ file containing options with which you can use to
+customize the build process. For example, which default backend to use, whether some of
+the optional libraries that Matplotlib ships with are installed, and so on. These
+options will be particularly useful to those packaging Matplotlib.
+
+.. _meson.options: https://github.com/matplotlib/matplotlib/blob/main/meson.options
+
+Aspects of some behaviorial defaults of the library can be configured via:
.. toctree::
:maxdepth: 2
diff --git a/galleries/examples/event_handling/path_editor.py b/galleries/examples/event_handling/path_editor.py
index d6e84b454008..2af54bad53ed 100644
--- a/galleries/examples/event_handling/path_editor.py
+++ b/galleries/examples/event_handling/path_editor.py
@@ -94,7 +94,6 @@ def on_draw(self, event):
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.draw_artist(self.pathpatch)
self.ax.draw_artist(self.line)
- self.canvas.blit(self.ax.bbox)
def on_button_press(self, event):
"""Callback for mouse button presses."""
diff --git a/galleries/examples/lines_bars_and_markers/multicolored_line.py b/galleries/examples/lines_bars_and_markers/multicolored_line.py
index 5d0727e69181..3d14ecaf8567 100644
--- a/galleries/examples/lines_bars_and_markers/multicolored_line.py
+++ b/galleries/examples/lines_bars_and_markers/multicolored_line.py
@@ -3,47 +3,188 @@
Multicolored lines
==================
-This example shows how to make a multicolored line. In this example, the line
-is colored based on its derivative.
+The example shows two ways to plot a line with the a varying color defined by
+a third value. The first example defines the color at each (x, y) point.
+The second example defines the color between pairs of points, so the length
+of the color value list is one less than the length of the x and y lists.
+
+Color values at points
+----------------------
+
"""
+import warnings
+
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.collections import LineCollection
-from matplotlib.colors import BoundaryNorm, ListedColormap
+
+def colored_line(x, y, c, ax, **lc_kwargs):
+ """
+ Plot a line with a color specified along the line by a third value.
+
+ It does this by creating a collection of line segments. Each line segment is
+ made up of two straight lines each connecting the current (x, y) point to the
+ midpoints of the lines connecting the current point with its two neighbors.
+ This creates a smooth line with no gaps between the line segments.
+
+ Parameters
+ ----------
+ x, y : array-like
+ The horizontal and vertical coordinates of the data points.
+ c : array-like
+ The color values, which should be the same size as x and y.
+ ax : Axes
+ Axis object on which to plot the colored line.
+ **lc_kwargs
+ Any additional arguments to pass to matplotlib.collections.LineCollection
+ constructor. This should not include the array keyword argument because
+ that is set to the color argument. If provided, it will be overridden.
+
+ Returns
+ -------
+ matplotlib.collections.LineCollection
+ The generated line collection representing the colored line.
+ """
+ if "array" in lc_kwargs:
+ warnings.warn('The provided "array" keyword argument will be overridden')
+
+ # Default the capstyle to butt so that the line segments smoothly line up
+ default_kwargs = {"capstyle": "butt"}
+ default_kwargs.update(lc_kwargs)
+
+ # Compute the midpoints of the line segments. Include the first and last points
+ # twice so we don't need any special syntax later to handle them.
+ x = np.asarray(x)
+ y = np.asarray(y)
+ x_midpts = np.hstack((x[0], 0.5 * (x[1:] + x[:-1]), x[-1]))
+ y_midpts = np.hstack((y[0], 0.5 * (y[1:] + y[:-1]), y[-1]))
+
+ # Determine the start, middle, and end coordinate pair of each line segment.
+ # Use the reshape to add an extra dimension so each pair of points is in its
+ # own list. Then concatenate them to create:
+ # [
+ # [(x1_start, y1_start), (x1_mid, y1_mid), (x1_end, y1_end)],
+ # [(x2_start, y2_start), (x2_mid, y2_mid), (x2_end, y2_end)],
+ # ...
+ # ]
+ coord_start = np.column_stack((x_midpts[:-1], y_midpts[:-1]))[:, np.newaxis, :]
+ coord_mid = np.column_stack((x, y))[:, np.newaxis, :]
+ coord_end = np.column_stack((x_midpts[1:], y_midpts[1:]))[:, np.newaxis, :]
+ segments = np.concatenate((coord_start, coord_mid, coord_end), axis=1)
+
+ lc = LineCollection(segments, **default_kwargs)
+ lc.set_array(c) # set the colors of each segment
+
+ return ax.add_collection(lc)
+
+
+# -------------- Create and show plot --------------
+# Some arbitrary function that gives x, y, and color values
+t = np.linspace(-7.4, -0.5, 200)
+x = 0.9 * np.sin(t)
+y = 0.9 * np.cos(1.6 * t)
+color = np.linspace(0, 2, t.size)
+
+# Create a figure and plot the line on it
+fig1, ax1 = plt.subplots()
+lines = colored_line(x, y, color, ax1, linewidth=10, cmap="plasma")
+fig1.colorbar(lines) # add a color legend
+
+# Set the axis limits and tick positions
+ax1.set_xlim(-1, 1)
+ax1.set_ylim(-1, 1)
+ax1.set_xticks((-1, 0, 1))
+ax1.set_yticks((-1, 0, 1))
+ax1.set_title("Color at each point")
+
+plt.show()
+
+####################################################################
+# This method is designed to give a smooth impression when distances and color
+# differences between adjacent points are not too large. The following example
+# does not meet this criteria and by that serves to illustrate the segmentation
+# and coloring mechanism.
+x = [0, 1, 2, 3, 4]
+y = [0, 1, 2, 1, 1]
+c = [1, 2, 3, 4, 5]
+fig, ax = plt.subplots()
+ax.scatter(x, y, c=c, cmap='rainbow')
+colored_line(x, y, c=c, ax=ax, cmap='rainbow')
+
+plt.show()
+
+####################################################################
+# Color values between points
+# ---------------------------
+#
+
+
+def colored_line_between_pts(x, y, c, ax, **lc_kwargs):
+ """
+ Plot a line with a color specified between (x, y) points by a third value.
+
+ It does this by creating a collection of line segments between each pair of
+ neighboring points. The color of each segment is determined by the
+ made up of two straight lines each connecting the current (x, y) point to the
+ midpoints of the lines connecting the current point with its two neighbors.
+ This creates a smooth line with no gaps between the line segments.
+
+ Parameters
+ ----------
+ x, y : array-like
+ The horizontal and vertical coordinates of the data points.
+ c : array-like
+ The color values, which should have a size one less than that of x and y.
+ ax : Axes
+ Axis object on which to plot the colored line.
+ **lc_kwargs
+ Any additional arguments to pass to matplotlib.collections.LineCollection
+ constructor. This should not include the array keyword argument because
+ that is set to the color argument. If provided, it will be overridden.
+
+ Returns
+ -------
+ matplotlib.collections.LineCollection
+ The generated line collection representing the colored line.
+ """
+ if "array" in lc_kwargs:
+ warnings.warn('The provided "array" keyword argument will be overridden')
+
+ # Check color array size (LineCollection still works, but values are unused)
+ if len(c) != len(x) - 1:
+ warnings.warn(
+ "The c argument should have a length one less than the length of x and y. "
+ "If it has the same length, use the colored_line function instead."
+ )
+
+ # Create a set of line segments so that we can color them individually
+ # This creates the points as an N x 1 x 2 array so that we can stack points
+ # together easily to get the segments. The segments array for line collection
+ # needs to be (numlines) x (points per line) x 2 (for x and y)
+ points = np.array([x, y]).T.reshape(-1, 1, 2)
+ segments = np.concatenate([points[:-1], points[1:]], axis=1)
+ lc = LineCollection(segments, **lc_kwargs)
+
+ # Set the values used for colormapping
+ lc.set_array(c)
+
+ return ax.add_collection(lc)
+
+
+# -------------- Create and show plot --------------
x = np.linspace(0, 3 * np.pi, 500)
y = np.sin(x)
dydx = np.cos(0.5 * (x[:-1] + x[1:])) # first derivative
-# Create a set of line segments so that we can color them individually
-# This creates the points as an N x 1 x 2 array so that we can stack points
-# together easily to get the segments. The segments array for line collection
-# needs to be (numlines) x (points per line) x 2 (for x and y)
-points = np.array([x, y]).T.reshape(-1, 1, 2)
-segments = np.concatenate([points[:-1], points[1:]], axis=1)
-
-fig, axs = plt.subplots(2, 1, sharex=True, sharey=True)
-
-# Create a continuous norm to map from data points to colors
-norm = plt.Normalize(dydx.min(), dydx.max())
-lc = LineCollection(segments, cmap='viridis', norm=norm)
-# Set the values used for colormapping
-lc.set_array(dydx)
-lc.set_linewidth(2)
-line = axs[0].add_collection(lc)
-fig.colorbar(line, ax=axs[0])
-
-# Use a boundary norm instead
-cmap = ListedColormap(['r', 'g', 'b'])
-norm = BoundaryNorm([-1, -0.5, 0.5, 1], cmap.N)
-lc = LineCollection(segments, cmap=cmap, norm=norm)
-lc.set_array(dydx)
-lc.set_linewidth(2)
-line = axs[1].add_collection(lc)
-fig.colorbar(line, ax=axs[1])
-
-axs[0].set_xlim(x.min(), x.max())
-axs[0].set_ylim(-1.1, 1.1)
+fig2, ax2 = plt.subplots()
+line = colored_line_between_pts(x, y, dydx, ax2, linewidth=2, cmap="viridis")
+fig2.colorbar(line, ax=ax2, label="dy/dx")
+
+ax2.set_xlim(x.min(), x.max())
+ax2.set_ylim(-1.1, 1.1)
+ax2.set_title("Color between points")
+
plt.show()
diff --git a/galleries/examples/mplot3d/imshow3d.py b/galleries/examples/mplot3d/imshow3d.py
new file mode 100644
index 000000000000..557d96e1bce5
--- /dev/null
+++ b/galleries/examples/mplot3d/imshow3d.py
@@ -0,0 +1,88 @@
+"""
+===============
+2D images in 3D
+===============
+
+This example demonstrates how to plot 2D color coded images (similar to
+`.Axes.imshow`) as a plane in 3D.
+
+Matplotlib does not have a native function for this. Below we build one by relying
+on `.Axes3D.plot_surface`. For simplicity, there are some differences to
+`.Axes.imshow`: This function does not set the aspect of the Axes, hence pixels are
+not necessarily square. Also, pixel edges are on integer values rather than pixel
+centers. Furthermore, many optional parameters of `.Axes.imshow` are not implemented.
+
+Multiple calls of ``imshow3d`` use independent norms and thus different color scales
+by default. If you want to have a single common color scale, you need to construct
+a suitable norm beforehand and pass it to all ``imshow3d`` calls.
+
+A fundamental limitation of the 3D plotting engine is that intersecting objects cannot
+be drawn correctly. One object will always be drawn after the other. Therefore,
+multiple image planes can well be used in the background as shown in this example.
+But this approach is not suitable if the planes intersect.
+"""
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+from matplotlib.colors import Normalize
+
+
+def imshow3d(ax, array, value_direction='z', pos=0, norm=None, cmap=None):
+ """
+ Display a 2D array as a color-coded 2D image embedded in 3d.
+
+ The image will be in a plane perpendicular to the coordinate axis *value_direction*.
+
+ Parameters
+ ----------
+ ax : Axes3D
+ The 3D Axes to plot into.
+ array : 2D numpy array
+ The image values.
+ value_direction : {'x', 'y', 'z'}
+ The axis normal to the image plane.
+ pos : float
+ The numeric value on the *value_direction* axis at which the image plane is
+ located.
+ norm : `~matplotlib.colors.Normalize`, default: Normalize
+ The normalization method used to scale scalar data. See `imshow()`.
+ cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap`
+ The Colormap instance or registered colormap name used to map scalar data
+ to colors.
+ """
+ if norm is None:
+ norm = Normalize()
+ colors = plt.get_cmap(cmap)(norm(array))
+
+ if value_direction == 'x':
+ nz, ny = array.shape
+ zi, yi = np.mgrid[0:nz + 1, 0:ny + 1]
+ xi = np.full_like(yi, pos)
+ elif value_direction == 'y':
+ nx, nz = array.shape
+ xi, zi = np.mgrid[0:nx + 1, 0:nz + 1]
+ yi = np.full_like(zi, pos)
+ elif value_direction == 'z':
+ ny, nx = array.shape
+ yi, xi = np.mgrid[0:ny + 1, 0:nx + 1]
+ zi = np.full_like(xi, pos)
+ else:
+ raise ValueError(f"Invalid value_direction: {value_direction!r}")
+ ax.plot_surface(xi, yi, zi, rstride=1, cstride=1, facecolors=colors, shade=False)
+
+
+fig = plt.figure()
+ax = fig.add_subplot(projection='3d')
+ax.set(xlabel="x", ylabel="y", zlabel="z")
+
+nx, ny, nz = 8, 10, 5
+data_xy = np.arange(ny * nx).reshape(ny, nx) + 15 * np.random.random((ny, nx))
+data_yz = np.arange(nz * ny).reshape(nz, ny) + 10 * np.random.random((nz, ny))
+data_zx = np.arange(nx * nz).reshape(nx, nz) + 8 * np.random.random((nx, nz))
+
+imshow3d(ax, data_xy)
+imshow3d(ax, data_yz, value_direction='x', cmap='magma')
+imshow3d(ax, data_zx, value_direction='y', pos=ny, cmap='plasma')
+
+plt.show()
diff --git a/galleries/examples/mplot3d/intersecting_planes.py b/galleries/examples/mplot3d/intersecting_planes.py
new file mode 100644
index 000000000000..b8aa08fd7e18
--- /dev/null
+++ b/galleries/examples/mplot3d/intersecting_planes.py
@@ -0,0 +1,89 @@
+"""
+===================
+Intersecting planes
+===================
+
+This examples demonstrates drawing intersecting planes in 3D. It is a generalization
+of :doc:`/gallery/mplot3d/imshow3d`.
+
+Drawing intersecting planes in `.mplot3d` is complicated, because `.mplot3d` is not a
+real 3D renderer, but only projects the Artists into 3D and draws them in the right
+order. This does not work correctly if Artists overlap each other mutually. In this
+example, we lift the problem of mutual overlap by segmenting the planes at their
+intersections, making four parts out of each plane.
+
+This examples only works correctly for planes that cut each other in haves. This
+limitation is intentional to keep the code more readable. Cutting at arbitrary
+positions would of course be possible but makes the code even more complex.
+Thus, this example is more a demonstration of the concept how to work around
+limitations of the 3D visualization, it's not a refined solution for drawing
+arbitrary intersecting planes, which you can copy-and-paste as is.
+"""
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+def plot_quadrants(ax, array, fixed_coord, cmap):
+ """For a given 3d *array* plot a plane with *fixed_coord*, using four quadrants."""
+ nx, ny, nz = array.shape
+ index = {
+ 'x': (nx // 2, slice(None), slice(None)),
+ 'y': (slice(None), ny // 2, slice(None)),
+ 'z': (slice(None), slice(None), nz // 2),
+ }[fixed_coord]
+ plane_data = array[index]
+
+ n0, n1 = plane_data.shape
+ quadrants = [
+ plane_data[:n0 // 2, :n1 // 2],
+ plane_data[:n0 // 2, n1 // 2:],
+ plane_data[n0 // 2:, :n1 // 2],
+ plane_data[n0 // 2:, n1 // 2:]
+ ]
+
+ min_val = array.min()
+ max_val = array.max()
+
+ cmap = plt.get_cmap(cmap)
+
+ for i, quadrant in enumerate(quadrants):
+ facecolors = cmap((quadrant - min_val) / (max_val - min_val))
+ if fixed_coord == 'x':
+ Y, Z = np.mgrid[0:ny // 2, 0:nz // 2]
+ X = nx // 2 * np.ones_like(Y)
+ Y_offset = (i // 2) * ny // 2
+ Z_offset = (i % 2) * nz // 2
+ ax.plot_surface(X, Y + Y_offset, Z + Z_offset, rstride=1, cstride=1,
+ facecolors=facecolors, shade=False)
+ elif fixed_coord == 'y':
+ X, Z = np.mgrid[0:nx // 2, 0:nz // 2]
+ Y = ny // 2 * np.ones_like(X)
+ X_offset = (i // 2) * nx // 2
+ Z_offset = (i % 2) * nz // 2
+ ax.plot_surface(X + X_offset, Y, Z + Z_offset, rstride=1, cstride=1,
+ facecolors=facecolors, shade=False)
+ elif fixed_coord == 'z':
+ X, Y = np.mgrid[0:nx // 2, 0:ny // 2]
+ Z = nz // 2 * np.ones_like(X)
+ X_offset = (i // 2) * nx // 2
+ Y_offset = (i % 2) * ny // 2
+ ax.plot_surface(X + X_offset, Y + Y_offset, Z, rstride=1, cstride=1,
+ facecolors=facecolors, shade=False)
+
+
+def figure_3D_array_slices(array, cmap=None):
+ """Plot a 3d array using three intersecting centered planes."""
+ fig = plt.figure()
+ ax = fig.add_subplot(projection='3d')
+ ax.set_box_aspect(array.shape)
+ plot_quadrants(ax, array, 'x', cmap=cmap)
+ plot_quadrants(ax, array, 'y', cmap=cmap)
+ plot_quadrants(ax, array, 'z', cmap=cmap)
+ return fig, ax
+
+
+nx, ny, nz = 70, 100, 50
+r_square = (np.mgrid[-1:1:1j * nx, -1:1:1j * ny, -1:1:1j * nz] ** 2).sum(0)
+
+figure_3D_array_slices(r_square, cmap='viridis_r')
+plt.show()
diff --git a/galleries/examples/units/basic_units.py b/galleries/examples/units/basic_units.py
index 0b493ab7216c..f9a94bcf6e37 100644
--- a/galleries/examples/units/basic_units.py
+++ b/galleries/examples/units/basic_units.py
@@ -146,10 +146,10 @@ def __getattribute__(self, name):
return getattr(variable, name)
return object.__getattribute__(self, name)
- def __array__(self, dtype=object):
+ def __array__(self, dtype=object, copy=False):
return np.asarray(self.value, dtype)
- def __array_wrap__(self, array, context):
+ def __array_wrap__(self, array, context=None, return_scalar=False):
return TaggedValue(array, self.unit)
def __repr__(self):
@@ -222,10 +222,10 @@ def __mul__(self, rhs):
def __rmul__(self, lhs):
return self*lhs
- def __array_wrap__(self, array, context):
+ def __array_wrap__(self, array, context=None, return_scalar=False):
return TaggedValue(array, self)
- def __array__(self, t=None, context=None):
+ def __array__(self, t=None, context=None, copy=False):
ret = np.array(1)
if t is not None:
return ret.astype(t)
diff --git a/galleries/plot_types/3D/bar3d_simple.py b/galleries/plot_types/3D/bar3d_simple.py
new file mode 100644
index 000000000000..aa75560de8f2
--- /dev/null
+++ b/galleries/plot_types/3D/bar3d_simple.py
@@ -0,0 +1,29 @@
+"""
+==========================
+bar3d(x, y, z, dx, dy, dz)
+==========================
+
+See `~mpl_toolkits.mplot3d.axes3d.Axes3D.bar3d`.
+"""
+import matplotlib.pyplot as plt
+import numpy as np
+
+plt.style.use('_mpl-gallery')
+
+# Make data
+x = [1, 1, 2, 2]
+y = [1, 2, 1, 2]
+z = [0, 0, 0, 0]
+dx = np.ones_like(x)*0.5
+dy = np.ones_like(x)*0.5
+dz = [2, 3, 1, 4]
+
+# Plot
+fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
+ax.bar3d(x, y, z, dx, dy, dz)
+
+ax.set(xticklabels=[],
+ yticklabels=[],
+ zticklabels=[])
+
+plt.show()
diff --git a/galleries/plot_types/3D/plot3d_simple.py b/galleries/plot_types/3D/plot3d_simple.py
new file mode 100644
index 000000000000..108dbecfbd87
--- /dev/null
+++ b/galleries/plot_types/3D/plot3d_simple.py
@@ -0,0 +1,27 @@
+"""
+================
+plot(xs, ys, zs)
+================
+
+See `~mpl_toolkits.mplot3d.axes3d.Axes3D.plot`.
+"""
+import matplotlib.pyplot as plt
+import numpy as np
+
+plt.style.use('_mpl-gallery')
+
+# Make data
+n = 100
+xs = np.linspace(0, 1, n)
+ys = np.sin(xs * 6 * np.pi)
+zs = np.cos(xs * 6 * np.pi)
+
+# Plot
+fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
+ax.plot(xs, ys, zs)
+
+ax.set(xticklabels=[],
+ yticklabels=[],
+ zticklabels=[])
+
+plt.show()
diff --git a/galleries/plot_types/3D/quiver3d_simple.py b/galleries/plot_types/3D/quiver3d_simple.py
new file mode 100644
index 000000000000..6f4aaa9cad90
--- /dev/null
+++ b/galleries/plot_types/3D/quiver3d_simple.py
@@ -0,0 +1,32 @@
+"""
+========================
+quiver(X, Y, Z, U, V, W)
+========================
+
+See `~mpl_toolkits.mplot3d.axes3d.Axes3D.quiver`.
+"""
+import matplotlib.pyplot as plt
+import numpy as np
+
+plt.style.use('_mpl-gallery')
+
+# Make data
+n = 4
+x = np.linspace(-1, 1, n)
+y = np.linspace(-1, 1, n)
+z = np.linspace(-1, 1, n)
+X, Y, Z = np.meshgrid(x, y, z)
+U = (X + Y)/5
+V = (Y - X)/5
+W = Z*0
+
+
+# Plot
+fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
+ax.quiver(X, Y, Z, U, V, W)
+
+ax.set(xticklabels=[],
+ yticklabels=[],
+ zticklabels=[])
+
+plt.show()
diff --git a/galleries/plot_types/3D/stem3d.py b/galleries/plot_types/3D/stem3d.py
new file mode 100644
index 000000000000..50aa80146bdc
--- /dev/null
+++ b/galleries/plot_types/3D/stem3d.py
@@ -0,0 +1,27 @@
+"""
+=============
+stem(x, y, z)
+=============
+
+See `~mpl_toolkits.mplot3d.axes3d.Axes3D.stem`.
+"""
+import matplotlib.pyplot as plt
+import numpy as np
+
+plt.style.use('_mpl-gallery')
+
+# Make data
+n = 20
+x = np.sin(np.linspace(0, 2*np.pi, n))
+y = np.cos(np.linspace(0, 2*np.pi, n))
+z = np.linspace(0, 1, n)
+
+# Plot
+fig, ax = plt.subplots(subplot_kw={"projection": "3d"})
+ax.stem(x, y, z)
+
+ax.set(xticklabels=[],
+ yticklabels=[],
+ zticklabels=[])
+
+plt.show()
diff --git a/galleries/plot_types/arrays/README.rst b/galleries/plot_types/arrays/README.rst
index d9dbfd10ead7..aba457a69940 100644
--- a/galleries/plot_types/arrays/README.rst
+++ b/galleries/plot_types/arrays/README.rst
@@ -1,7 +1,7 @@
.. _array_plots:
-Gridded data:
--------------
+Gridded data
+------------
Plots of arrays and images :math:`Z_{i, j}` and fields :math:`U_{i, j}, V_{i, j}`
on `regular grids `_ and
diff --git a/galleries/plot_types/arrays/imshow.py b/galleries/plot_types/arrays/imshow.py
index c28278c6c657..b2920e7fd80c 100644
--- a/galleries/plot_types/arrays/imshow.py
+++ b/galleries/plot_types/arrays/imshow.py
@@ -19,6 +19,6 @@
# plot
fig, ax = plt.subplots()
-ax.imshow(Z)
+ax.imshow(Z, origin='lower')
plt.show()
diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py
index b65004b8c272..fdafc2dcb0bc 100644
--- a/lib/matplotlib/axes/_axes.py
+++ b/lib/matplotlib/axes/_axes.py
@@ -1006,14 +1006,14 @@ def axhspan(self, ymin, ymax, xmin=0, xmax=1, **kwargs):
Returns
-------
- `~matplotlib.patches.Polygon`
+ `~matplotlib.patches.Rectangle`
Horizontal span (rectangle) from (xmin, ymin) to (xmax, ymax).
Other Parameters
----------------
- **kwargs : `~matplotlib.patches.Polygon` properties
+ **kwargs : `~matplotlib.patches.Rectangle` properties
- %(Polygon:kwdoc)s
+ %(Rectangle:kwdoc)s
See Also
--------
@@ -1061,14 +1061,14 @@ def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs):
Returns
-------
- `~matplotlib.patches.Polygon`
+ `~matplotlib.patches.Rectangle`
Vertical span (rectangle) from (xmin, ymin) to (xmax, ymax).
Other Parameters
----------------
- **kwargs : `~matplotlib.patches.Polygon` properties
+ **kwargs : `~matplotlib.patches.Rectangle` properties
- %(Polygon:kwdoc)s
+ %(Rectangle:kwdoc)s
See Also
--------
@@ -1586,7 +1586,7 @@ def plot(self, *args, scalex=True, scaley=True, data=None, **kwargs):
>>> plot(x1, y1, 'bo')
>>> plot(x2, y2, 'go')
- - If *x* and/or *y* are 2D arrays a separate data set will be drawn
+ - If *x* and/or *y* are 2D arrays, a separate data set will be drawn
for every column. If both *x* and *y* are 2D, they must have the
same shape. If only one of them is 2D with shape (N, m) the other
must have length N and will be used for every data set m.
diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi
index b70d330aa442..76aaee77aff8 100644
--- a/lib/matplotlib/axes/_axes.pyi
+++ b/lib/matplotlib/axes/_axes.pyi
@@ -155,10 +155,10 @@ class Axes(_AxesBase):
) -> AxLine: ...
def axhspan(
self, ymin: float, ymax: float, xmin: float = ..., xmax: float = ..., **kwargs
- ) -> Polygon: ...
+ ) -> Rectangle: ...
def axvspan(
self, xmin: float, xmax: float, ymin: float = ..., ymax: float = ..., **kwargs
- ) -> Polygon: ...
+ ) -> Rectangle: ...
def hlines(
self,
y: float | ArrayLike,
diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py
index 0164f4e11169..30c4efe80c49 100644
--- a/lib/matplotlib/axes/_base.py
+++ b/lib/matplotlib/axes/_base.py
@@ -597,7 +597,8 @@ def __init__(self, fig,
sharex, sharey : `~matplotlib.axes.Axes`, optional
The x- or y-`~.matplotlib.axis` is shared with the x- or y-axis in
- the input `~.axes.Axes`.
+ the input `~.axes.Axes`. Note that it is not possible to unshare
+ axes.
frameon : bool, default: True
Whether the Axes frame is visible.
@@ -1221,7 +1222,7 @@ def sharex(self, other):
This is equivalent to passing ``sharex=other`` when constructing the
Axes, and cannot be used if the x-axis is already being shared with
- another Axes.
+ another Axes. Note that it is not possible to unshare axes.
"""
_api.check_isinstance(_AxesBase, other=other)
if self._sharex is not None and other is not self._sharex:
@@ -1240,7 +1241,7 @@ def sharey(self, other):
This is equivalent to passing ``sharey=other`` when constructing the
Axes, and cannot be used if the y-axis is already being shared with
- another Axes.
+ another Axes. Note that it is not possible to unshare axes.
"""
_api.check_isinstance(_AxesBase, other=other)
if self._sharey is not None and other is not self._sharey:
diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py
index f4273bc03919..53e5f6b23213 100644
--- a/lib/matplotlib/backend_bases.py
+++ b/lib/matplotlib/backend_bases.py
@@ -513,21 +513,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
If True, use mathtext parser. If "TeX", use tex for rendering.
mtext : `~matplotlib.text.Text`
The original text object to be rendered.
-
- Notes
- -----
- **Note for backend implementers:**
-
- When you are trying to determine if you have gotten your bounding box
- right (which is what enables the text layout/alignment to work
- properly), it helps to change the line in text.py::
-
- if 0: bbox_artist(self, renderer)
-
- to if 1, and then the actual bounding box will be plotted along with
- your text.
"""
-
self._draw_text_as_path(gc, x, y, s, prop, angle, ismath)
def _get_text_path_transform(self, x, y, s, prop, angle, ismath):
diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py
index 295f6c41372d..df06440a9826 100644
--- a/lib/matplotlib/backends/_backend_tk.py
+++ b/lib/matplotlib/backends/_backend_tk.py
@@ -44,7 +44,7 @@ def _restore_foreground_window_at_end():
try:
yield
finally:
- if mpl.rcParams['tk.window_focus']:
+ if foreground and mpl.rcParams['tk.window_focus']:
_c_internal_utils.Win32_SetForegroundWindow(foreground)
diff --git a/lib/matplotlib/backends/registry.py b/lib/matplotlib/backends/registry.py
index 19b4cba254ab..47d5f65e350e 100644
--- a/lib/matplotlib/backends/registry.py
+++ b/lib/matplotlib/backends/registry.py
@@ -168,8 +168,11 @@ def backward_compatible_entry_points(
def _validate_and_store_entry_points(self, entries):
# Validate and store entry points so that they can be used via matplotlib.use()
# in the normal manner. Entry point names cannot be of module:// format, cannot
- # shadow a built-in backend name, and cannot be duplicated.
- for name, module in entries:
+ # shadow a built-in backend name, and there cannot be multiple entry points
+ # with the same name but different modules. Multiple entry points with the same
+ # name and value are permitted (it can sometimes happen outside of our control,
+ # see https://github.com/matplotlib/matplotlib/issues/28367).
+ for name, module in set(entries):
name = name.lower()
if name.startswith("module://"):
raise RuntimeError(
diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py
index af61e4671ff4..156ea2ff6497 100644
--- a/lib/matplotlib/colorbar.py
+++ b/lib/matplotlib/colorbar.py
@@ -257,10 +257,6 @@ class Colorbar:
*location* is None, the ticks will be at the bottom for a horizontal
colorbar and at the right for a vertical.
- drawedges : bool
- Whether to draw lines at color boundaries.
-
-
%(_colormap_kw_doc)s
location : None or {'left', 'right', 'top', 'bottom'}
diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py
index c4e5987fdf92..177557b371a6 100644
--- a/lib/matplotlib/colors.py
+++ b/lib/matplotlib/colors.py
@@ -225,7 +225,7 @@ def is_color_like(c):
return True
try:
to_rgba(c)
- except ValueError:
+ except (TypeError, ValueError):
return False
else:
return True
@@ -296,6 +296,11 @@ def to_rgba(c, alpha=None):
Tuple of floats ``(r, g, b, a)``, where each channel (red, green, blue,
alpha) can assume values between 0 and 1.
"""
+ if isinstance(c, tuple) and len(c) == 2:
+ if alpha is None:
+ c, alpha = c
+ else:
+ c = c[0]
# Special-case nth color syntax because it should not be cached.
if _is_nth_color(c):
prop_cycler = mpl.rcParams['axes.prop_cycle']
@@ -325,11 +330,6 @@ def _to_rgba_no_colorcycle(c, alpha=None):
*alpha* is ignored for the color value ``"none"`` (case-insensitive),
which always maps to ``(0, 0, 0, 0)``.
"""
- if isinstance(c, tuple) and len(c) == 2:
- if alpha is None:
- c, alpha = c
- else:
- c = c[0]
if alpha is not None and not 0 <= alpha <= 1:
raise ValueError("'alpha' must be between 0 and 1, inclusive")
orig_c = c
diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py
index 0a0ff01a2571..e5f4bb9421cf 100644
--- a/lib/matplotlib/figure.py
+++ b/lib/matplotlib/figure.py
@@ -755,6 +755,8 @@ def subplots(self, nrows=1, ncols=1, *, sharex=False, sharey=False,
When subplots have a shared axis that has units, calling
`.Axis.set_units` will update each axis with the new units.
+ Note that it is not possible to unshare axes.
+
squeeze : bool, default: True
- If True, extra dimensions are squeezed out from the returned
array of Axes:
diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi
index eae21c2614f0..21de9159d56c 100644
--- a/lib/matplotlib/figure.pyi
+++ b/lib/matplotlib/figure.pyi
@@ -1,12 +1,12 @@
from collections.abc import Callable, Hashable, Iterable
import os
-from typing import Any, IO, Literal, TypeVar, overload
+from typing import Any, IO, Literal, Sequence, TypeVar, overload
import numpy as np
from numpy.typing import ArrayLike
from matplotlib.artist import Artist
-from matplotlib.axes import Axes, SubplotBase
+from matplotlib.axes import Axes
from matplotlib.backend_bases import (
FigureCanvasBase,
MouseButton,
@@ -92,6 +92,20 @@ class FigureBase(Artist):
@overload
def add_subplot(self, **kwargs) -> Axes: ...
@overload
+ def subplots(
+ self,
+ nrows: Literal[1] = ...,
+ ncols: Literal[1] = ...,
+ *,
+ sharex: bool | Literal["none", "all", "row", "col"] = ...,
+ sharey: bool | Literal["none", "all", "row", "col"] = ...,
+ squeeze: Literal[True] = ...,
+ width_ratios: Sequence[float] | None = ...,
+ height_ratios: Sequence[float] | None = ...,
+ subplot_kw: dict[str, Any] | None = ...,
+ gridspec_kw: dict[str, Any] | None = ...,
+ ) -> Axes: ...
+ @overload
def subplots(
self,
nrows: int = ...,
@@ -100,11 +114,11 @@ class FigureBase(Artist):
sharex: bool | Literal["none", "all", "row", "col"] = ...,
sharey: bool | Literal["none", "all", "row", "col"] = ...,
squeeze: Literal[False],
- width_ratios: ArrayLike | None = ...,
- height_ratios: ArrayLike | None = ...,
+ width_ratios: Sequence[float] | None = ...,
+ height_ratios: Sequence[float] | None = ...,
subplot_kw: dict[str, Any] | None = ...,
- gridspec_kw: dict[str, Any] | None = ...
- ) -> np.ndarray: ...
+ gridspec_kw: dict[str, Any] | None = ...,
+ ) -> np.ndarray: ... # TODO numpy/numpy#24738
@overload
def subplots(
self,
@@ -114,11 +128,11 @@ class FigureBase(Artist):
sharex: bool | Literal["none", "all", "row", "col"] = ...,
sharey: bool | Literal["none", "all", "row", "col"] = ...,
squeeze: bool = ...,
- width_ratios: ArrayLike | None = ...,
- height_ratios: ArrayLike | None = ...,
+ width_ratios: Sequence[float] | None = ...,
+ height_ratios: Sequence[float] | None = ...,
subplot_kw: dict[str, Any] | None = ...,
- gridspec_kw: dict[str, Any] | None = ...
- ) -> np.ndarray | SubplotBase | Axes: ...
+ gridspec_kw: dict[str, Any] | None = ...,
+ ) -> Axes | np.ndarray: ...
def delaxes(self, ax: Axes) -> None: ...
def clear(self, keep_observers: bool = ...) -> None: ...
def clf(self, keep_observers: bool = ...) -> None: ...
diff --git a/lib/matplotlib/gridspec.pyi b/lib/matplotlib/gridspec.pyi
index 1ac1bb0b40e7..b6732ad8fafa 100644
--- a/lib/matplotlib/gridspec.pyi
+++ b/lib/matplotlib/gridspec.pyi
@@ -54,7 +54,7 @@ class GridSpecBase:
sharey: bool | Literal["all", "row", "col", "none"] = ...,
squeeze: Literal[True] = ...,
subplot_kw: dict[str, Any] | None = ...
- ) -> np.ndarray | SubplotBase | Axes: ...
+ ) -> np.ndarray | Axes: ...
class GridSpec(GridSpecBase):
left: float | None
diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py
index b1354341617d..8fe8b000bf49 100644
--- a/lib/matplotlib/pyplot.py
+++ b/lib/matplotlib/pyplot.py
@@ -90,6 +90,9 @@
import PIL.Image
from numpy.typing import ArrayLike
+ import matplotlib.axes
+ import matplotlib.artist
+ import matplotlib.backend_bases
from matplotlib.axis import Tick
from matplotlib.axes._base import _AxesBase
from matplotlib.backend_bases import RendererBase, Event
@@ -301,10 +304,12 @@ def install_repl_displayhook() -> None:
# This code can be removed when Python 3.12, the latest version supported by
# IPython < 8.24, reaches end-of-life in late 2028.
from IPython.core.pylabtools import backend2gui
- # trigger IPython's eventloop integration, if available
ipython_gui_name = backend2gui.get(get_backend())
- if ipython_gui_name:
- ip.enable_gui(ipython_gui_name)
+ else:
+ _, ipython_gui_name = backend_registry.resolve_backend(get_backend())
+ # trigger IPython's eventloop integration, if available
+ if ipython_gui_name:
+ ip.enable_gui(ipython_gui_name)
def uninstall_repl_displayhook() -> None:
@@ -1545,6 +1550,57 @@ def subplot(*args, **kwargs) -> Axes:
return ax
+@overload
+def subplots(
+ nrows: Literal[1] = ...,
+ ncols: Literal[1] = ...,
+ *,
+ sharex: bool | Literal["none", "all", "row", "col"] = ...,
+ sharey: bool | Literal["none", "all", "row", "col"] = ...,
+ squeeze: Literal[True] = ...,
+ width_ratios: Sequence[float] | None = ...,
+ height_ratios: Sequence[float] | None = ...,
+ subplot_kw: dict[str, Any] | None = ...,
+ gridspec_kw: dict[str, Any] | None = ...,
+ **fig_kw
+) -> tuple[Figure, Axes]:
+ ...
+
+
+@overload
+def subplots(
+ nrows: int = ...,
+ ncols: int = ...,
+ *,
+ sharex: bool | Literal["none", "all", "row", "col"] = ...,
+ sharey: bool | Literal["none", "all", "row", "col"] = ...,
+ squeeze: Literal[False],
+ width_ratios: Sequence[float] | None = ...,
+ height_ratios: Sequence[float] | None = ...,
+ subplot_kw: dict[str, Any] | None = ...,
+ gridspec_kw: dict[str, Any] | None = ...,
+ **fig_kw
+) -> tuple[Figure, np.ndarray]: # TODO numpy/numpy#24738
+ ...
+
+
+@overload
+def subplots(
+ nrows: int = ...,
+ ncols: int = ...,
+ *,
+ sharex: bool | Literal["none", "all", "row", "col"] = ...,
+ sharey: bool | Literal["none", "all", "row", "col"] = ...,
+ squeeze: bool = ...,
+ width_ratios: Sequence[float] | None = ...,
+ height_ratios: Sequence[float] | None = ...,
+ subplot_kw: dict[str, Any] | None = ...,
+ gridspec_kw: dict[str, Any] | None = ...,
+ **fig_kw
+) -> tuple[Figure, Axes | np.ndarray]:
+ ...
+
+
def subplots(
nrows: int = 1, ncols: int = 1, *,
sharex: bool | Literal["none", "all", "row", "col"] = False,
@@ -1583,8 +1639,9 @@ def subplots(
on, use `~matplotlib.axes.Axes.tick_params`.
When subplots have a shared axis that has units, calling
- `~matplotlib.axis.Axis.set_units` will update each axis with the
- new units.
+ `.Axis.set_units` will update each axis with the new units.
+
+ Note that it is not possible to unshare axes.
squeeze : bool, default: True
- If True, extra dimensions are squeezed out from the returned
@@ -2813,7 +2870,7 @@ def axhline(y: float = 0, xmin: float = 0, xmax: float = 1, **kwargs) -> Line2D:
@_copy_docstring_and_deprecators(Axes.axhspan)
def axhspan(
ymin: float, ymax: float, xmin: float = 0, xmax: float = 1, **kwargs
-) -> Polygon:
+) -> Rectangle:
return gca().axhspan(ymin, ymax, xmin=xmin, xmax=xmax, **kwargs)
@@ -2851,7 +2908,7 @@ def axvline(x: float = 0, ymin: float = 0, ymax: float = 1, **kwargs) -> Line2D:
@_copy_docstring_and_deprecators(Axes.axvspan)
def axvspan(
xmin: float, xmax: float, ymin: float = 0, ymax: float = 1, **kwargs
-) -> Polygon:
+) -> Rectangle:
return gca().axvspan(xmin, xmax, ymin=ymin, ymax=ymax, **kwargs)
diff --git a/lib/matplotlib/tests/test_backend_registry.py b/lib/matplotlib/tests/test_backend_registry.py
index eaf8417e7a5f..141ffd69c266 100644
--- a/lib/matplotlib/tests/test_backend_registry.py
+++ b/lib/matplotlib/tests/test_backend_registry.py
@@ -121,6 +121,17 @@ def test_entry_point_name_duplicate(clear_backend_registry):
[('some_name', 'module1'), ('some_name', 'module2')])
+def test_entry_point_identical(clear_backend_registry):
+ # Issue https://github.com/matplotlib/matplotlib/issues/28367
+ # Multiple entry points with the same name and value (value is the module)
+ # are acceptable.
+ n = len(backend_registry._name_to_module)
+ backend_registry._validate_and_store_entry_points(
+ [('some_name', 'some.module'), ('some_name', 'some.module')])
+ assert len(backend_registry._name_to_module) == n+1
+ assert backend_registry._name_to_module['some_name'] == 'module://some.module'
+
+
def test_entry_point_name_is_module(clear_backend_registry):
with pytest.raises(RuntimeError):
backend_registry._validate_and_store_entry_points(
diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py
index 63f2d4f00399..4fd9f86c06e3 100644
--- a/lib/matplotlib/tests/test_colors.py
+++ b/lib/matplotlib/tests/test_colors.py
@@ -19,7 +19,7 @@
import matplotlib.scale as mscale
from matplotlib.rcsetup import cycler
from matplotlib.testing.decorators import image_comparison, check_figures_equal
-from matplotlib.colors import to_rgba_array
+from matplotlib.colors import is_color_like, to_rgba_array
@pytest.mark.parametrize('N, result', [
@@ -1697,3 +1697,16 @@ def test_to_rgba_array_none_color_with_alpha_param():
assert_array_equal(
to_rgba_array(c, alpha), [[0., 0., 1., 1.], [0., 0., 0., 0.]]
)
+
+
+@pytest.mark.parametrize('input, expected',
+ [('red', True),
+ (('red', 0.5), True),
+ (('red', 2), False),
+ (['red', 0.5], False),
+ (('red', 'blue'), False),
+ (['red', 'blue'], False),
+ ('C3', True),
+ (('C3', 0.5), True)])
+def test_is_color_like(input, expected):
+ assert is_color_like(input) is expected
diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py
index f8837d8a5f1b..8904337f68ba 100644
--- a/lib/matplotlib/tests/test_text.py
+++ b/lib/matplotlib/tests/test_text.py
@@ -15,6 +15,7 @@
from matplotlib.font_manager import FontProperties
import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
+from matplotlib.gridspec import GridSpec
import matplotlib.transforms as mtransforms
from matplotlib.testing.decorators import check_figures_equal, image_comparison
from matplotlib.testing._markers import needs_usetex
@@ -707,9 +708,13 @@ def test_large_subscript_title():
(0.3, 0, 'right'),
(0.3, 185, 'left')])
def test_wrap(x, rotation, halign):
- fig = plt.figure(figsize=(6, 6))
+ fig = plt.figure(figsize=(18, 18))
+ gs = GridSpec(nrows=3, ncols=3, figure=fig)
+ subfig = fig.add_subfigure(gs[1, 1])
+ # we only use the central subfigure, which does not align with any
+ # figure boundary, to ensure only subfigure boundaries are relevant
s = 'This is a very long text that should be wrapped multiple times.'
- text = fig.text(x, 0.7, s, wrap=True, rotation=rotation, ha=halign)
+ text = subfig.text(x, 0.7, s, wrap=True, rotation=rotation, ha=halign)
fig.canvas.draw()
assert text._get_wrapped_text() == ('This is a very long\n'
'text that should be\n'
diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py
index 36b83c95b3d3..ac68a5d90b14 100644
--- a/lib/matplotlib/tests/test_ticker.py
+++ b/lib/matplotlib/tests/test_ticker.py
@@ -130,6 +130,14 @@ def test_view_limits_round_numbers_with_offset(self):
loc = mticker.MultipleLocator(base=3.147, offset=1.3)
assert_almost_equal(loc.view_limits(-4, 4), (-4.994, 4.447))
+ def test_view_limits_single_bin(self):
+ """
+ Test that 'round_numbers' works properly with a single bin.
+ """
+ with mpl.rc_context({'axes.autolimit_mode': 'round_numbers'}):
+ loc = mticker.MaxNLocator(nbins=1)
+ assert_almost_equal(loc.view_limits(-2.3, 2.3), (-4, 4))
+
def test_set_params(self):
"""
Create multiple locator with 0.7 base, and change it to something else.
diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py
index 40cd8c8cd6f7..af990ec1bf9f 100644
--- a/lib/matplotlib/text.py
+++ b/lib/matplotlib/text.py
@@ -606,6 +606,9 @@ def set_wrap(self, wrap):
"""
Set whether the text can be wrapped.
+ Wrapping makes sure the text is confined to the (sub)figure box. It
+ does not take into account any other artists.
+
Parameters
----------
wrap : bool
@@ -653,16 +656,16 @@ def _get_dist_to_box(self, rotation, x0, y0, figure_box):
"""
if rotation > 270:
quad = rotation - 270
- h1 = y0 / math.cos(math.radians(quad))
+ h1 = (y0 - figure_box.y0) / math.cos(math.radians(quad))
h2 = (figure_box.x1 - x0) / math.cos(math.radians(90 - quad))
elif rotation > 180:
quad = rotation - 180
- h1 = x0 / math.cos(math.radians(quad))
- h2 = y0 / math.cos(math.radians(90 - quad))
+ h1 = (x0 - figure_box.x0) / math.cos(math.radians(quad))
+ h2 = (y0 - figure_box.y0) / math.cos(math.radians(90 - quad))
elif rotation > 90:
quad = rotation - 90
h1 = (figure_box.y1 - y0) / math.cos(math.radians(quad))
- h2 = x0 / math.cos(math.radians(90 - quad))
+ h2 = (x0 - figure_box.x0) / math.cos(math.radians(90 - quad))
else:
h1 = (figure_box.x1 - x0) / math.cos(math.radians(rotation))
h2 = (figure_box.y1 - y0) / math.cos(math.radians(90 - rotation))
diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py
index f042372a7be9..2b00937f9e29 100644
--- a/lib/matplotlib/ticker.py
+++ b/lib/matplotlib/ticker.py
@@ -2137,7 +2137,10 @@ def _raw_ticks(self, vmin, vmax):
large_steps = large_steps & (floored_vmaxs >= _vmax)
# Find index of smallest large step
- istep = np.nonzero(large_steps)[0][0]
+ if any(large_steps):
+ istep = np.nonzero(large_steps)[0][0]
+ else:
+ istep = len(steps) - 1
# Start at smallest of the steps greater than the raw step, and check
# if it provides enough ticks. If not, work backwards through
diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py
index eaa35e25440b..ed130e6854f2 100644
--- a/lib/matplotlib/widgets.py
+++ b/lib/matplotlib/widgets.py
@@ -1001,14 +1001,8 @@ class CheckButtons(AxesWidget):
----------
ax : `~matplotlib.axes.Axes`
The parent Axes for the widget.
-
labels : list of `~matplotlib.text.Text`
-
- rectangles : list of `~matplotlib.patches.Rectangle`
-
- lines : list of (`.Line2D`, `.Line2D`) pairs
- List of lines for the x's in the checkboxes. These lines exist for
- each box, but have ``set_visible(False)`` when its box is not checked.
+ The text label objects of the check buttons.
"""
def __init__(self, ax, labels, actives=None, *, useblit=True,
@@ -1571,8 +1565,6 @@ class RadioButtons(AxesWidget):
The color of the selected button.
labels : list of `.Text`
The button labels.
- circles : list of `~.patches.Circle`
- The buttons.
value_selected : str
The label text of the currently selected button.
index_selected : int
@@ -1751,11 +1743,6 @@ def activecolor(self, activecolor):
colors._check_color_like(activecolor=activecolor)
self._activecolor = activecolor
self.set_radio_props({'facecolor': activecolor})
- # Make sure the deprecated version is updated.
- # Remove once circles is removed.
- labels = [label.get_text() for label in self.labels]
- with cbook._setattr_cm(self, eventson=False):
- self.set_active(labels.index(self.value_selected))
def set_active(self, index):
"""
diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py
index d0f5c8d2b23b..71cd8f062d40 100644
--- a/lib/mpl_toolkits/mplot3d/axes3d.py
+++ b/lib/mpl_toolkits/mplot3d/axes3d.py
@@ -83,7 +83,8 @@ def __init__(
axis. A positive angle spins the camera clockwise, causing the
scene to rotate counter-clockwise.
sharez : Axes3D, optional
- Other Axes to share z-limits with.
+ Other Axes to share z-limits with. Note that it is not possible to
+ unshare axes.
proj_type : {'persp', 'ortho'}
The projection type, default 'persp'.
box_aspect : 3-tuple of floats, default: None
@@ -107,7 +108,8 @@ def __init__(
The focal length can be computed from a desired Field Of View via
the equation: focal_length = 1/tan(FOV/2)
shareview : Axes3D, optional
- Other Axes to share view angles with.
+ Other Axes to share view angles with. Note that it is not possible
+ to unshare axes.
**kwargs
Other optional keyword arguments:
@@ -383,7 +385,7 @@ def set_box_aspect(self, aspect, *, zoom=1):
# of the axes in mpl3.8.
aspect *= 1.8294640721620434 * 25/24 * zoom / np.linalg.norm(aspect)
- self._box_aspect = aspect
+ self._box_aspect = self._roll_to_vertical(aspect, reverse=True)
self.stale = True
def apply_aspect(self, position=None):
@@ -1191,9 +1193,23 @@ def set_proj_type(self, proj_type, focal_length=None):
f"None for proj_type = {proj_type}")
self._focal_length = np.inf
- def _roll_to_vertical(self, arr):
- """Roll arrays to match the different vertical axis."""
- return np.roll(arr, self._vertical_axis - 2)
+ def _roll_to_vertical(
+ self, arr: "np.typing.ArrayLike", reverse: bool = False
+ ) -> np.ndarray:
+ """
+ Roll arrays to match the different vertical axis.
+
+ Parameters
+ ----------
+ arr : ArrayLike
+ Array to roll.
+ reverse : bool, default: False
+ Reverse the direction of the roll.
+ """
+ if reverse:
+ return np.roll(arr, (self._vertical_axis - 2) * -1)
+ else:
+ return np.roll(arr, (self._vertical_axis - 2))
def get_proj(self):
"""Create the projection matrix from the current viewing position."""
@@ -1293,7 +1309,7 @@ def sharez(self, other):
This is equivalent to passing ``sharez=other`` when constructing the
Axes, and cannot be used if the z-axis is already being shared with
- another Axes.
+ another Axes. Note that it is not possible to unshare axes.
"""
_api.check_isinstance(Axes3D, other=other)
if self._sharez is not None and other is not self._sharez:
@@ -1310,9 +1326,9 @@ def shareview(self, other):
"""
Share the view angles with *other*.
- This is equivalent to passing ``shareview=other`` when
- constructing the Axes, and cannot be used if the view angles are
- already being shared with another Axes.
+ This is equivalent to passing ``shareview=other`` when constructing the
+ Axes, and cannot be used if the view angles are already being shared
+ with another Axes. Note that it is not possible to unshare axes.
"""
_api.check_isinstance(Axes3D, other=other)
if self._shareview is not None and other is not self._shareview:
@@ -1524,6 +1540,7 @@ def _on_move(self, event):
dazim = -(dy/h)*180*np.sin(roll) - (dx/w)*180*np.cos(roll)
elev = self.elev + delev
azim = self.azim + dazim
+ roll = self.roll
vertical_axis = self._axis_names[self._vertical_axis]
self.view_init(
elev=elev,
@@ -2561,7 +2578,7 @@ def tricontourf(self, *args, zdir='z', offset=None, **kwargs):
self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data)
return cset
- def add_collection3d(self, col, zs=0, zdir='z'):
+ def add_collection3d(self, col, zs=0, zdir='z', autolim=True):
"""
Add a 3D collection object to the plot.
@@ -2573,8 +2590,21 @@ def add_collection3d(self, col, zs=0, zdir='z'):
- `.PolyCollection`
- `.LineCollection`
- - `.PatchCollection`
+ - `.PatchCollection` (currently not supporting *autolim*)
+
+ Parameters
+ ----------
+ col : `.Collection`
+ A 2D collection object.
+ zs : float or array-like, default: 0
+ The z-positions to be used for the 2D objects.
+ zdir : {'x', 'y', 'z'}, default: 'z'
+ The direction to use for the z-positions.
+ autolim : bool, default: True
+ Whether to update the data limits.
"""
+ had_data = self.has_data()
+
zvals = np.atleast_1d(zs)
zsortval = (np.min(zvals) if zvals.size
else 0) # FIXME: arbitrary default
@@ -2592,6 +2622,18 @@ def add_collection3d(self, col, zs=0, zdir='z'):
art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir)
col.set_sort_zpos(zsortval)
+ if autolim:
+ if isinstance(col, art3d.Line3DCollection):
+ self.auto_scale_xyz(*np.array(col._segments3d).transpose(),
+ had_data=had_data)
+ elif isinstance(col, art3d.Poly3DCollection):
+ self.auto_scale_xyz(*col._vec[:-1], had_data=had_data)
+ elif isinstance(col, art3d.Patch3DCollection):
+ pass
+ # FIXME: Implement auto-scaling function for Patch3DCollection
+ # Currently unable to do so due to issues with Patch3DCollection
+ # See https://github.com/matplotlib/matplotlib/issues/14298 for details
+
collection = super().add_collection(col)
return collection
diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-named-colors.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-named-colors.png
index b71ad19c1608..33dfc2f2313a 100644
Binary files a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-named-colors.png and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-named-colors.png differ
diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py
index ed56e5505d8e..ecb51b724c27 100644
--- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py
+++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py
@@ -961,8 +961,8 @@ def test_poly3dcollection_closed():
facecolor=(0.5, 0.5, 1, 0.5), closed=True)
c2 = art3d.Poly3DCollection([poly2], linewidths=3, edgecolor='k',
facecolor=(1, 0.5, 0.5, 0.5), closed=False)
- ax.add_collection3d(c1)
- ax.add_collection3d(c2)
+ ax.add_collection3d(c1, autolim=False)
+ ax.add_collection3d(c2, autolim=False)
def test_poly_collection_2d_to_3d_empty():
@@ -995,8 +995,8 @@ def test_poly3dcollection_alpha():
c2.set_facecolor((1, 0.5, 0.5))
c2.set_edgecolor('k')
c2.set_alpha(0.5)
- ax.add_collection3d(c1)
- ax.add_collection3d(c2)
+ ax.add_collection3d(c1, autolim=False)
+ ax.add_collection3d(c2, autolim=False)
@mpl3d_image_comparison(['add_collection3d_zs_array.png'], style='mpl20')
@@ -1055,6 +1055,32 @@ def test_add_collection3d_zs_scalar():
ax.set_zlim(0, 2)
+def test_line3dCollection_autoscaling():
+ fig = plt.figure()
+ ax = fig.add_subplot(projection='3d')
+
+ lines = [[(0, 0, 0), (1, 4, 2)],
+ [(1, 1, 3), (2, 0, 2)],
+ [(1, 0, 4), (1, 4, 5)]]
+
+ lc = art3d.Line3DCollection(lines)
+ ax.add_collection3d(lc)
+ assert np.allclose(ax.get_xlim3d(), (-0.041666666666666664, 2.0416666666666665))
+ assert np.allclose(ax.get_ylim3d(), (-0.08333333333333333, 4.083333333333333))
+ assert np.allclose(ax.get_zlim3d(), (-0.10416666666666666, 5.104166666666667))
+
+
+def test_poly3dCollection_autoscaling():
+ fig = plt.figure()
+ ax = fig.add_subplot(projection='3d')
+ poly = np.array([[0, 0, 0], [1, 1, 3], [1, 0, 4]])
+ col = art3d.Poly3DCollection([poly])
+ ax.add_collection3d(col)
+ assert np.allclose(ax.get_xlim3d(), (-0.020833333333333332, 1.0208333333333333))
+ assert np.allclose(ax.get_ylim3d(), (-0.020833333333333332, 1.0208333333333333))
+ assert np.allclose(ax.get_zlim3d(), (-0.0833333333333333, 4.083333333333333))
+
+
@mpl3d_image_comparison(['axes3d_labelpad.png'],
remove_text=False, style='mpl20')
def test_axes3d_labelpad():
@@ -1766,6 +1792,31 @@ def test_shared_axes_retick():
assert ax2.get_zlim() == (-0.5, 2.5)
+def test_rotate():
+ """Test rotating using the left mouse button."""
+ for roll in [0, 30]:
+ fig = plt.figure()
+ ax = fig.add_subplot(1, 1, 1, projection='3d')
+ ax.view_init(0, 0, roll)
+ ax.figure.canvas.draw()
+
+ # drag mouse horizontally to change azimuth
+ dx = 0.1
+ dy = 0.2
+ ax._button_press(
+ mock_event(ax, button=MouseButton.LEFT, xdata=0, ydata=0))
+ ax._on_move(
+ mock_event(ax, button=MouseButton.LEFT,
+ xdata=dx*ax._pseudo_w, ydata=dy*ax._pseudo_h))
+ ax.figure.canvas.draw()
+ roll_radians = np.deg2rad(ax.roll)
+ cs = np.cos(roll_radians)
+ sn = np.sin(roll_radians)
+ assert ax.elev == (-dy*180*cs + dx*180*sn)
+ assert ax.azim == (-dy*180*sn - dx*180*cs)
+ assert ax.roll == roll
+
+
def test_pan():
"""Test mouse panning using the middle mouse button."""
@@ -2276,6 +2327,24 @@ def test_on_move_vertical_axis(vertical_axis: str) -> None:
)
+@pytest.mark.parametrize(
+ "vertical_axis, aspect_expected",
+ [
+ ("x", [1.190476, 0.892857, 1.190476]),
+ ("y", [0.892857, 1.190476, 1.190476]),
+ ("z", [1.190476, 1.190476, 0.892857]),
+ ],
+)
+def test_set_box_aspect_vertical_axis(vertical_axis, aspect_expected):
+ ax = plt.subplot(1, 1, 1, projection="3d")
+ ax.view_init(elev=0, azim=0, roll=0, vertical_axis=vertical_axis)
+ ax.figure.canvas.draw()
+
+ ax.set_box_aspect(None)
+
+ np.testing.assert_allclose(aspect_expected, ax._box_aspect, rtol=1e-6)
+
+
@image_comparison(baseline_images=['arc_pathpatch.png'],
remove_text=True,
style='mpl20')
diff --git a/pyproject.toml b/pyproject.toml
index fe75b325dc89..a9fb7df68450 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -95,7 +95,6 @@ install = ['--tags=data,python-runtime,runtime']
[tool.setuptools_scm]
version_scheme = "release-branch-semver"
local_scheme = "node-and-date"
-write_to = "lib/matplotlib/_version.py"
parentdir_prefix_version = "matplotlib-"
fallback_version = "0.0+UNKNOWN"
diff --git a/src/_c_internal_utils.cpp b/src/_c_internal_utils.cpp
index 464aabcb2e3a..e118183ecc8b 100644
--- a/src/_c_internal_utils.cpp
+++ b/src/_c_internal_utils.cpp
@@ -111,7 +111,11 @@ static py::object
mpl_GetForegroundWindow(void)
{
#ifdef _WIN32
- return py::capsule(GetForegroundWindow(), "HWND");
+ if (HWND hwnd = GetForegroundWindow()) {
+ return py::capsule(hwnd, "HWND");
+ } else {
+ return py::none();
+ }
#else
return py::none();
#endif
diff --git a/src/checkdep_freetype2.c b/src/checkdep_freetype2.c
index 8d9d8ca24a07..16e8ac23919e 100644
--- a/src/checkdep_freetype2.c
+++ b/src/checkdep_freetype2.c
@@ -1,7 +1,7 @@
#ifdef __has_include
#if !__has_include()
#error "FreeType version 2.3 or higher is required. \
-You may unset the system_freetype entry in mplsetup.cfg to let Matplotlib download it."
+You may set the system-freetype Meson build option to false to let Matplotlib download it."
#endif
#endif
@@ -15,5 +15,5 @@ You may unset the system_freetype entry in mplsetup.cfg to let Matplotlib downlo
XSTR(FREETYPE_MAJOR) "." XSTR(FREETYPE_MINOR) "." XSTR(FREETYPE_PATCH) ".")
#if FREETYPE_MAJOR << 16 + FREETYPE_MINOR << 8 + FREETYPE_PATCH < 0x020300
#error "FreeType version 2.3 or higher is required. \
-You may unset the system_freetype entry in mplsetup.cfg to let Matplotlib download it."
+You may set the system-freetype Meson build option to false to let Matplotlib download it."
#endif