From 3b85ba4365c8eaa172d0a92c9d3ecc2d51693c42 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 9 May 2025 12:23:58 -0500 Subject: [PATCH 001/110] Zenodo v3.10.3 --- doc/_static/zenodo_cache/14940554.svg | 35 +++++++++++++++++++++++++++ doc/_static/zenodo_cache/15375714.svg | 35 +++++++++++++++++++++++++++ doc/project/citing.rst | 6 +++++ tools/cache_zenodo_svg.py | 2 ++ 4 files changed, 78 insertions(+) create mode 100644 doc/_static/zenodo_cache/14940554.svg create mode 100644 doc/_static/zenodo_cache/15375714.svg diff --git a/doc/_static/zenodo_cache/14940554.svg b/doc/_static/zenodo_cache/14940554.svg new file mode 100644 index 000000000000..6e7d5c37bf7b --- /dev/null +++ b/doc/_static/zenodo_cache/14940554.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.14940554 + + + 10.5281/zenodo.14940554 + + + \ No newline at end of file diff --git a/doc/_static/zenodo_cache/15375714.svg b/doc/_static/zenodo_cache/15375714.svg new file mode 100644 index 000000000000..d5e403138561 --- /dev/null +++ b/doc/_static/zenodo_cache/15375714.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.15375714 + + + 10.5281/zenodo.15375714 + + + \ No newline at end of file diff --git a/doc/project/citing.rst b/doc/project/citing.rst index 2cd317906bb5..8b4c323229ca 100644 --- a/doc/project/citing.rst +++ b/doc/project/citing.rst @@ -32,6 +32,12 @@ By version .. START OF AUTOGENERATED +v3.10.3 + .. image:: ../_static/zenodo_cache/15375714.svg + :target: https://doi.org/10.5281/zenodo.15375714 +v3.10.1 + .. image:: ../_static/zenodo_cache/14940554.svg + :target: https://doi.org/10.5281/zenodo.14940554 v3.10.0 .. image:: ../_static/zenodo_cache/14464227.svg :target: https://doi.org/10.5281/zenodo.14464227 diff --git a/tools/cache_zenodo_svg.py b/tools/cache_zenodo_svg.py index 3be7d6ca21e4..229e90efeb34 100644 --- a/tools/cache_zenodo_svg.py +++ b/tools/cache_zenodo_svg.py @@ -63,6 +63,8 @@ def _get_xdg_cache_dir(): if __name__ == "__main__": data = { + "v3.10.3": "15375714", + "v3.10.1": "14940554", "v3.10.0": "14464227", "v3.9.4": "14436121", "v3.9.3": "14249941", From d3c77afde1e3fd6948d15ae730d0cebfe25fd6fe Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 9 May 2025 22:46:21 +0200 Subject: [PATCH 002/110] Backport PR #30029: Update diagram in subplots_adjust documentation to clarify parameters --- doc/_embedded_plots/figure_subplots_adjust.py | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/doc/_embedded_plots/figure_subplots_adjust.py b/doc/_embedded_plots/figure_subplots_adjust.py index b4b8d7d32a3d..6f99a3febcdc 100644 --- a/doc/_embedded_plots/figure_subplots_adjust.py +++ b/doc/_embedded_plots/figure_subplots_adjust.py @@ -1,28 +1,34 @@ import matplotlib.pyplot as plt -def arrow(p1, p2, **props): - axs[0, 0].annotate( - "", p1, p2, xycoords='figure fraction', - arrowprops=dict(arrowstyle="<->", shrinkA=0, shrinkB=0, **props)) - - fig, axs = plt.subplots(2, 2, figsize=(6.5, 4)) fig.set_facecolor('lightblue') fig.subplots_adjust(0.1, 0.1, 0.9, 0.9, 0.4, 0.4) + +overlay = fig.add_axes([0, 0, 1, 1], zorder=100) +overlay.axis("off") +xycoords='figure fraction' +arrowprops=dict(arrowstyle="<->", shrinkA=0, shrinkB=0) + for ax in axs.flat: ax.set(xticks=[], yticks=[]) -arrow((0, 0.75), (0.1, 0.75)) # left -arrow((0.435, 0.75), (0.565, 0.75)) # wspace -arrow((0.9, 0.75), (1, 0.75)) # right +overlay.annotate("", (0, 0.75), (0.1, 0.75), + xycoords=xycoords, arrowprops=arrowprops) # left +overlay.annotate("", (0.435, 0.25), (0.565, 0.25), + xycoords=xycoords, arrowprops=arrowprops) # wspace +overlay.annotate("", (0, 0.8), (0.9, 0.8), + xycoords=xycoords, arrowprops=arrowprops) # right fig.text(0.05, 0.7, "left", ha="center") -fig.text(0.5, 0.7, "wspace", ha="center") -fig.text(0.95, 0.7, "right", ha="center") +fig.text(0.5, 0.3, "wspace", ha="center") +fig.text(0.05, 0.83, "right", ha="center") -arrow((0.25, 0), (0.25, 0.1)) # bottom -arrow((0.25, 0.435), (0.25, 0.565)) # hspace -arrow((0.25, 0.9), (0.25, 1)) # top -fig.text(0.28, 0.05, "bottom", va="center") +overlay.annotate("", (0.75, 0), (0.75, 0.1), + xycoords=xycoords, arrowprops=arrowprops) # bottom +overlay.annotate("", (0.25, 0.435), (0.25, 0.565), + xycoords=xycoords, arrowprops=arrowprops) # hspace +overlay.annotate("", (0.8, 0), (0.8, 0.9), + xycoords=xycoords, arrowprops=arrowprops) # top +fig.text(0.65, 0.05, "bottom", va="center") fig.text(0.28, 0.5, "hspace", va="center") -fig.text(0.28, 0.95, "top", va="center") +fig.text(0.82, 0.05, "top", va="center") From b81427cabae98716eda5d27134a882682754de26 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 9 May 2025 23:06:22 +0200 Subject: [PATCH 003/110] Update doc/_embedded_plots/figure_subplots_adjust.py Fix flake8 --- doc/_embedded_plots/figure_subplots_adjust.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/_embedded_plots/figure_subplots_adjust.py b/doc/_embedded_plots/figure_subplots_adjust.py index 6f99a3febcdc..d32a029fe05d 100644 --- a/doc/_embedded_plots/figure_subplots_adjust.py +++ b/doc/_embedded_plots/figure_subplots_adjust.py @@ -7,8 +7,8 @@ overlay = fig.add_axes([0, 0, 1, 1], zorder=100) overlay.axis("off") -xycoords='figure fraction' -arrowprops=dict(arrowstyle="<->", shrinkA=0, shrinkB=0) +xycoords = 'figure fraction' +arrowprops = dict(arrowstyle="<->", shrinkA=0, shrinkB=0) for ax in axs.flat: ax.set(xticks=[], yticks=[]) From e188484b9ccacbda63c401fe925e93a10d73eac8 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 4 Jun 2025 16:11:33 -0400 Subject: [PATCH 004/110] Backport PR #30118: CI: Skip jobs on forks --- .github/workflows/cibuildwheel.yml | 38 +++++++++++++++------------ .github/workflows/codeql-analysis.yml | 1 + .github/workflows/conflictcheck.yml | 1 + 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 79d770d0c8f4..3eddbb402e6a 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -24,14 +24,16 @@ permissions: jobs: build_sdist: if: >- - github.event_name == 'push' || - github.event_name == 'pull_request' && ( - ( - github.event.action == 'labeled' && - github.event.label.name == 'CI: Run cibuildwheel' - ) || - contains(github.event.pull_request.labels.*.name, - 'CI: Run cibuildwheel') + github.repository == 'matplotlib/matplotlib' && ( + github.event_name == 'push' || + github.event_name == 'pull_request' && ( + ( + github.event.action == 'labeled' && + github.event.label.name == 'CI: Run cibuildwheel' + ) || + contains(github.event.pull_request.labels.*.name, + 'CI: Run cibuildwheel') + ) ) name: Build sdist runs-on: ubuntu-latest @@ -78,14 +80,16 @@ jobs: build_wheels: if: >- - github.event_name == 'push' || - github.event_name == 'pull_request' && ( - ( - github.event.action == 'labeled' && - github.event.label.name == 'CI: Run cibuildwheel' - ) || - contains(github.event.pull_request.labels.*.name, - 'CI: Run cibuildwheel') + github.repository == 'matplotlib/matplotlib' && ( + github.event_name == 'push' || + github.event_name == 'pull_request' && ( + ( + github.event.action == 'labeled' && + github.event.label.name == 'CI: Run cibuildwheel' + ) || + contains(github.event.pull_request.labels.*.name, + 'CI: Run cibuildwheel') + ) ) needs: build_sdist name: Build wheels on ${{ matrix.os }} for ${{ matrix.cibw_archs }} @@ -188,7 +192,7 @@ jobs: if-no-files-found: error publish: - if: github.event_name == 'push' && github.ref_type == 'tag' + if: github.repository == 'matplotlib/matplotlib' && github.event_name == 'push' && github.ref_type == 'tag' name: Upload release to PyPI needs: [build_sdist, build_wheels] runs-on: ubuntu-latest diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 089b15700f1b..7a15de609834 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -12,6 +12,7 @@ on: jobs: analyze: + if: github.repository == 'matplotlib/matplotlib' name: Analyze runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/conflictcheck.yml b/.github/workflows/conflictcheck.yml index b018101f325c..c9aa036004e7 100644 --- a/.github/workflows/conflictcheck.yml +++ b/.github/workflows/conflictcheck.yml @@ -11,6 +11,7 @@ on: jobs: main: + if: github.repository == 'matplotlib/matplotlib' runs-on: ubuntu-latest permissions: pull-requests: write From 5bdbb3ad9d982f58174f0ae10323364760ece24e Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 30 May 2025 01:50:50 -0400 Subject: [PATCH 005/110] Backport PR #30119: Add some types to _mathtext.py --- lib/matplotlib/_mathtext.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 7085a986414e..9e20ea3da9b7 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -2521,10 +2521,10 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: if len(new_children): # remove last kern if (isinstance(new_children[-1], Kern) and - hasattr(new_children[-2], '_metrics')): + isinstance(new_children[-2], Char)): new_children = new_children[:-1] last_char = new_children[-1] - if hasattr(last_char, '_metrics'): + if isinstance(last_char, Char): last_char.width = last_char._metrics.advance # create new Hlist without kerning nucleus = Hlist(new_children, do_kern=False) @@ -2600,7 +2600,7 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: # Do we need to add a space after the nucleus? # To find out, check the flag set by operatorname - spaced_nucleus = [nucleus, x] + spaced_nucleus: list[Node] = [nucleus, x] if self._in_subscript_or_superscript: spaced_nucleus += [self._make_space(self._space_widths[r'\,'])] self._in_subscript_or_superscript = False From 3950d996236581bf7d53f29ffd7fc33434687552 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Thu, 19 Jun 2025 15:09:33 -0700 Subject: [PATCH 006/110] Backport PR #30180: DOC: expand polar example --- .../pie_and_polar_charts/polar_demo.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/galleries/examples/pie_and_polar_charts/polar_demo.py b/galleries/examples/pie_and_polar_charts/polar_demo.py index e4967079d19d..909fea094be5 100644 --- a/galleries/examples/pie_and_polar_charts/polar_demo.py +++ b/galleries/examples/pie_and_polar_charts/polar_demo.py @@ -4,6 +4,11 @@ ========== Demo of a line plot on a polar axis. + +The second plot shows the same data, but with the radial axis starting at r=1 +and the angular axis starting at 0 degrees and ending at 225 degrees. Setting +the origin of the radial axis to 0 allows the radial ticks to be placed at the +same location as the first plot. """ import matplotlib.pyplot as plt import numpy as np @@ -11,14 +16,29 @@ r = np.arange(0, 2, 0.01) theta = 2 * np.pi * r -fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) +fig, axs = plt.subplots(2, 1, figsize=(5, 8), subplot_kw={'projection': 'polar'}, + layout='constrained') +ax = axs[0] ax.plot(theta, r) ax.set_rmax(2) -ax.set_rticks([0.5, 1, 1.5, 2]) # Less radial ticks +ax.set_rticks([0.5, 1, 1.5, 2]) # Fewer radial ticks ax.set_rlabel_position(-22.5) # Move radial labels away from plotted line ax.grid(True) ax.set_title("A line plot on a polar axis", va='bottom') + +ax = axs[1] +ax.plot(theta, r) +ax.set_rmax(2) +ax.set_rmin(1) # Change the radial axis to only go from 1 to 2 +ax.set_rorigin(0) # Set the origin of the radial axis to 0 +ax.set_thetamin(0) +ax.set_thetamax(225) +ax.set_rticks([1, 1.5, 2]) # Fewer radial ticks +ax.set_rlabel_position(-22.5) # Move radial labels away from plotted line + +ax.grid(True) +ax.set_title("Same plot, but with reduced axis limits", va='bottom') plt.show() # %% @@ -32,6 +52,8 @@ # - `matplotlib.projections.polar` # - `matplotlib.projections.polar.PolarAxes` # - `matplotlib.projections.polar.PolarAxes.set_rticks` +# - `matplotlib.projections.polar.PolarAxes.set_rmin` +# - `matplotlib.projections.polar.PolarAxes.set_rorigin` # - `matplotlib.projections.polar.PolarAxes.set_rmax` # - `matplotlib.projections.polar.PolarAxes.set_rlabel_position` # From 96512fd4835d61e037974944b355edcc131b5f46 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 25 Jun 2025 09:08:40 +0200 Subject: [PATCH 007/110] Backport PR #30212: [Doc]: fix bug in release notes for matplotlib v3.5.0 and v3.7.0 --- doc/api/prev_api_changes/api_changes_3.5.0/deprecations.rst | 2 +- doc/api/prev_api_changes/api_changes_3.7.0/removals.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/api/prev_api_changes/api_changes_3.5.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.5.0/deprecations.rst index d10da55a97f8..742a18f04072 100644 --- a/doc/api/prev_api_changes/api_changes_3.5.0/deprecations.rst +++ b/doc/api/prev_api_changes/api_changes_3.5.0/deprecations.rst @@ -282,7 +282,7 @@ Miscellaneous deprecations - The *format* parameter of ``dviread.find_tex_file`` is deprecated (with no replacement). - ``FancyArrowPatch.get_path_in_displaycoord`` and - ``ConnectionPath.get_path_in_displaycoord`` are deprecated. The path in + ``ConnectionPatch.get_path_in_displaycoord`` are deprecated. The path in display coordinates can still be obtained, as for other patches, using ``patch.get_transform().transform_path(patch.get_path())``. - The ``font_manager.win32InstalledFonts`` and diff --git a/doc/api/prev_api_changes/api_changes_3.7.0/removals.rst b/doc/api/prev_api_changes/api_changes_3.7.0/removals.rst index 03239be31057..56b3ad5c253e 100644 --- a/doc/api/prev_api_changes/api_changes_3.7.0/removals.rst +++ b/doc/api/prev_api_changes/api_changes_3.7.0/removals.rst @@ -323,7 +323,7 @@ Miscellaneous removals - The *format* parameter of ``dviread.find_tex_file`` is removed (with no replacement). - ``FancyArrowPatch.get_path_in_displaycoord`` and - ``ConnectionPath.get_path_in_displaycoord`` are removed. The path in + ``ConnectionPatch.get_path_in_displaycoord`` are removed. The path in display coordinates can still be obtained, as for other patches, using ``patch.get_transform().transform_path(patch.get_path())``. - The ``font_manager.win32InstalledFonts`` and From b1226396211d5280dcecf55929f2df3fd62cc8a0 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 2 Jul 2025 07:40:25 -0700 Subject: [PATCH 008/110] Backport PR #30244: DOC: Recommend to use bare Figure instances for saving to file --- doc/users/faq.rst | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/doc/users/faq.rst b/doc/users/faq.rst index 592fff551099..87a1af03d96e 100644 --- a/doc/users/faq.rst +++ b/doc/users/faq.rst @@ -281,8 +281,23 @@ locators as desired because the two axes are independent. Generate images without having a window appear ---------------------------------------------- -Simply do not call `~matplotlib.pyplot.show`, and directly save the figure to -the desired format:: +The recommended approach since matplotlib 3.1 is to explicitly create a Figure +instance:: + + from matplotlib.figure import Figure + fig = Figure() + ax = fig.subplots() + ax.plot([1, 2, 3]) + fig.savefig('myfig.png') + +This prevents any interaction with GUI frameworks and the window manager. + +It's alternatively still possible to use the pyplot interface. Instead of +calling `matplotlib.pyplot.show`, call `matplotlib.pyplot.savefig`. + +Additionally, you must ensure to close the figure after saving it. Not +closing the figure is a memory leak, because pyplot keeps references +to all not-yet-shown figures:: import matplotlib.pyplot as plt plt.plot([1, 2, 3]) From 74d8632fa71aadc21037de776284e26aa4c2351d Mon Sep 17 00:00:00 2001 From: hannah Date: Fri, 11 Jul 2025 14:11:06 -0400 Subject: [PATCH 009/110] Backport PR #30289: DOC: Fix build with pybind11 3 --- doc/missing-references.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/missing-references.json b/doc/missing-references.json index 1a816d19f7cd..e3b23db6fbc8 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -261,8 +261,12 @@ "doc/api/_as_gen/mpl_toolkits.axisartist.floating_axes.rst:32::1" ], "numpy.float64": [ + "doc/docstring of matplotlib.ft2font.pybind11_detail_function_record_v1_system_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_1.set_text:1", "doc/docstring of matplotlib.ft2font.PyCapsule.set_text:1" ], + "numpy.typing.NDArray": [ + "doc/docstring of matplotlib.ft2font.pybind11_detail_function_record_v1_system_libstdcpp_gxx_abi_1xxx_use_cxx11_abi_1.set_text:1" + ], "numpy.uint8": [ ":1" ] From bc3117798d70f72219dfb08a624f33780a309e20 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 1 Apr 2025 23:41:13 +0200 Subject: [PATCH 010/110] ENH: Allow to register standalone figures with pyplot It may be fundamentally nice not to have to create the figure though pyplot to be able to use it in pyplot afterwards. You can now do ``` from matplotlib.figure import Figure import matplotlib.pyplot as plt fig = Figure() fig.subplots().plot([1, 3, 2]) plt.figure(fig) # fig is now tracked in pyplot plt.show() ``` This also opens up the possibility to more dynamically track and untrack figures in pyplot, which opens up the road to optimized figure tracking in pyplot (#29849) --- lib/matplotlib/pyplot.py | 23 +++++++++++++++++++---- lib/matplotlib/tests/test_figure.py | 2 -- lib/matplotlib/tests/test_pyplot.py | 24 ++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index e916d57f8871..e6c609c2b84a 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -933,6 +933,10 @@ def figure( window title is set to this value. If num is a ``SubFigure``, its parent ``Figure`` is activated. + If *num* is a Figure instance that is already tracked in pyplot, it is + activated. If *num* is a Figure instance that is not tracked in pyplot, + it is added to the tracked figures and activated. + figsize : (float, float) or (float, float, str), default: :rc:`figure.figsize` The figure dimensions. This can be @@ -1019,21 +1023,32 @@ def figure( in the matplotlibrc file. """ allnums = get_fignums() + next_num = max(allnums) + 1 if allnums else 1 if isinstance(num, FigureBase): # type narrowed to `Figure | SubFigure` by combination of input and isinstance + has_figure_property_parameters = ( + any(param is not None for param in [figsize, dpi, facecolor, edgecolor]) + or not frameon or kwargs + ) + root_fig = num.get_figure(root=True) if root_fig.canvas.manager is None: - raise ValueError("The passed figure is not managed by pyplot") - elif (any(param is not None for param in [figsize, dpi, facecolor, edgecolor]) - or not frameon or kwargs) and root_fig.canvas.manager.num in allnums: + if has_figure_property_parameters: + raise ValueError( + "You cannot pass figure properties when calling figure() with " + "an existing Figure instance") + backend = _get_backend_mod() + manager_ = backend.new_figure_manager_given_figure(next_num, root_fig) + _pylab_helpers.Gcf._set_new_active_manager(manager_) + return manager_.canvas.figure + elif has_figure_property_parameters and root_fig.canvas.manager.num in allnums: _api.warn_external( "Ignoring specified arguments in this call because figure " f"with num: {root_fig.canvas.manager.num} already exists") _pylab_helpers.Gcf.set_active(root_fig.canvas.manager) return root_fig - next_num = max(allnums) + 1 if allnums else 1 fig_label = '' if num is None: num = next_num diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index c5890a2963b3..5f0e68648966 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -147,8 +147,6 @@ def test_figure_label(): assert plt.get_figlabels() == ['', 'today'] plt.figure(fig_today) assert plt.gcf() == fig_today - with pytest.raises(ValueError): - plt.figure(Figure()) def test_figure_label_replaced(): diff --git a/lib/matplotlib/tests/test_pyplot.py b/lib/matplotlib/tests/test_pyplot.py index 55f7c33cb52e..44555a333a8c 100644 --- a/lib/matplotlib/tests/test_pyplot.py +++ b/lib/matplotlib/tests/test_pyplot.py @@ -471,6 +471,30 @@ def test_multiple_same_figure_calls(): assert fig is fig3 +def test_register_existing_figure_with_pyplot(): + from matplotlib.figure import Figure + # start with a standalone figure + fig = Figure() + assert fig.canvas.manager is None + with pytest.raises(AttributeError): + # Heads-up: This will change to returning None in the future + # See docstring for the Figure.number property + fig.number + # register the Figure with pyplot + plt.figure(fig) + assert fig.number == 1 + # the figure can now be used in pyplot + plt.suptitle("my title") + assert fig.get_suptitle() == "my title" + # it also has a manager that is properly wired up in the pyplot state + assert plt._pylab_helpers.Gcf.get_fig_manager(fig.number) is fig.canvas.manager + # and we can regularly switch the pyplot state + fig2 = plt.figure() + assert fig2.number == 2 + assert plt.figure(1) is fig + assert plt.gcf() is fig + + def test_close_all_warning(): fig1 = plt.figure() From c718eae4dd7761051ba309d5c962077797f1e954 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 27 Jul 2025 23:30:52 +0200 Subject: [PATCH 011/110] Clarifying comment --- lib/matplotlib/_pylab_helpers.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/matplotlib/_pylab_helpers.py b/lib/matplotlib/_pylab_helpers.py index e3c3d98cb156..05f6d8aa02b3 100644 --- a/lib/matplotlib/_pylab_helpers.py +++ b/lib/matplotlib/_pylab_helpers.py @@ -53,6 +53,8 @@ def destroy(cls, num): two managers share the same number. """ if all(hasattr(num, attr) for attr in ["num", "destroy"]): + # num is a manager-like instance (not necessarily a + # FigureManagerBase subclass) manager = num if cls.figs.get(manager.num) is manager: cls.figs.pop(manager.num) From a38f9bb624f59cc01320840a5a53b6ab32e72867 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 27 Jul 2025 23:32:44 +0200 Subject: [PATCH 012/110] Fix: properly decouple from pyplot and specific backends when destroying When destroying a manager, replace the figure's canvas by a figure canvas base. --- lib/matplotlib/backend_bases.py | 4 +++- lib/matplotlib/backends/_backend_gtk.py | 1 + lib/matplotlib/backends/_backend_tk.py | 1 + lib/matplotlib/backends/backend_gtk3.py | 1 + lib/matplotlib/backends/backend_gtk4.py | 1 + lib/matplotlib/backends/backend_nbagg.py | 1 + lib/matplotlib/backends/backend_qt.py | 1 + lib/matplotlib/backends/backend_wx.py | 1 + lib/matplotlib/figure.py | 11 ++++++++++- lib/matplotlib/tests/test_backends_interactive.py | 9 ++++++--- 10 files changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 4adaecb7f8c0..51db8dc054e5 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2723,7 +2723,9 @@ def show(self): f"shown") def destroy(self): - pass + # managers may have swapped the canvas to a GUI-framework specific one. + # restore the base canvas when the manager is destroyed. + self.canvas.figure._set_base_canvas() def full_screen_toggle(self): pass diff --git a/lib/matplotlib/backends/_backend_gtk.py b/lib/matplotlib/backends/_backend_gtk.py index ac443730e28a..ce6982a72526 100644 --- a/lib/matplotlib/backends/_backend_gtk.py +++ b/lib/matplotlib/backends/_backend_gtk.py @@ -195,6 +195,7 @@ def destroy(self, *args): self._destroying = True self.window.destroy() self.canvas.destroy() + super().destroy() @classmethod def start_main_loop(cls): diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index eaf868fd8bec..3cd349cb9e17 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -606,6 +606,7 @@ def delayed_destroy(): else: self.window.update() delayed_destroy() + super().destroy() def get_window_title(self): return self.window.wm_title() diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 888f5a770f5d..68ba3a329b5e 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -89,6 +89,7 @@ def __init__(self, figure=None): def destroy(self): CloseEvent("close_event", self)._process() + super().destroy() def set_cursor(self, cursor): # docstring inherited diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index cd38968779ed..8c9045e8cca3 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -90,6 +90,7 @@ def __init__(self, figure=None): def destroy(self): CloseEvent("close_event", self)._process() + super().destroy() def set_cursor(self, cursor): # docstring inherited diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index 4d18e1e9fb88..543454ab25fd 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -137,6 +137,7 @@ def _create_comm(self): return comm def destroy(self): + super().destroy() self._send_event('close') # need to copy comms as callbacks will modify this list for comm in list(self.web_sockets): diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 401ce0b0b754..68d89e1990bb 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -664,6 +664,7 @@ def show(self): self.window.raise_() def destroy(self, *args): + super().destroy() # check for qApp first, as PySide deletes it in its atexit handler if QtWidgets.QApplication.instance() is None: return diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index f83a69d8361e..5219042e7971 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -1007,6 +1007,7 @@ def show(self): def destroy(self, *args): # docstring inherited _log.debug("%s - destroy()", type(self)) + super().destroy() frame = self.frame if frame: # Else, may have been already deleted, e.g. when closing. # As this can be called from non-GUI thread from plt.close use diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 03549dd53bc1..00a21343eef6 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -2642,7 +2642,7 @@ def __init__(self, self._set_artist_props(self.patch) self.patch.set_antialiased(False) - FigureCanvasBase(self) # Set self.canvas. + self._set_base_canvas() if subplotpars is None: subplotpars = SubplotParams() @@ -2996,6 +2996,15 @@ def get_constrained_layout_pads(self, relative=False): return w_pad, h_pad, wspace, hspace + def _set_base_canvas(self): + """ + Initialize self.canvas with a FigureCanvasBase instance. + + This is used upon initialization of the Figure, but also + to reset the canvas when decoupling from pyplot. + """ + FigureCanvasBase(self) # Set self.canvas as a side-effect + def set_canvas(self, canvas): """ Set the canvas that contains the figure diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 9f8522a9df4a..5a6740d6e7ac 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -227,12 +227,15 @@ def check_alt_backend(alt_backend): # Ensure that the window is really closed. plt.pause(0.5) - # Test that saving works after interactive window is closed, but the figure - # is not deleted. + # When the figure is closed, it's manager is removed and the canvas is reset to + # FigureCanvasBase. Saving should still be possible. result_after = io.BytesIO() fig.savefig(result_after, format='png') - assert result.getvalue() == result_after.getvalue() + if backend.endswith("agg"): + # agg-based interactive backends should save the same image as a non-interactive + # figure + assert result.getvalue() == result_after.getvalue() @pytest.mark.parametrize("env", _get_testable_interactive_backends()) From eab4a8934249af016cfde396a1de55f897e3690a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 30 Jul 2025 10:34:00 -0400 Subject: [PATCH 013/110] TST: do not use non-baseclass methods to get renderer in tests There in now machinery for all of the public API that takes a renderer is input to get it from the current canvas on the root figure. Use that machinery instead. --- lib/matplotlib/tests/test_axes.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index c96173e340f7..e0b651095cb5 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -8223,10 +8223,9 @@ def color_boxes(fig, ax): """ fig.canvas.draw() - renderer = fig.canvas.get_renderer() bbaxis = [] for nn, axx in enumerate([ax.xaxis, ax.yaxis]): - bb = axx.get_tightbbox(renderer) + bb = axx.get_tightbbox() if bb: axisr = mpatches.Rectangle( (bb.x0, bb.y0), width=bb.width, height=bb.height, @@ -8237,7 +8236,7 @@ def color_boxes(fig, ax): bbspines = [] for nn, a in enumerate(['bottom', 'top', 'left', 'right']): - bb = ax.spines[a].get_window_extent(renderer) + bb = ax.spines[a].get_window_extent() spiner = mpatches.Rectangle( (bb.x0, bb.y0), width=bb.width, height=bb.height, linewidth=0.7, edgecolor="green", facecolor="none", transform=None, @@ -8253,7 +8252,7 @@ def color_boxes(fig, ax): fig.add_artist(rect2) bbax = bb - bb2 = ax.get_tightbbox(renderer) + bb2 = ax.get_tightbbox() rect2 = mpatches.Rectangle( (bb2.x0, bb2.y0), width=bb2.width, height=bb2.height, linewidth=3, edgecolor="red", facecolor="none", transform=None, From 2396e9011c0b7da4e230ebc09b8e204b51672e99 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 30 Jul 2025 11:05:52 -0400 Subject: [PATCH 014/110] TST: fix wx window closing test Flush the wx canvas, not the base canvas that is installed on close. --- lib/matplotlib/tests/test_backends_interactive.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 5a6740d6e7ac..0a1c7f703cad 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -288,10 +288,13 @@ def _test_thread_impl(): future = ThreadPoolExecutor().submit(fig.canvas.draw) plt.pause(0.5) # flush_events fails here on at least Tkagg (bpo-41176) future.result() # Joins the thread; rethrows any exception. + # stash the current canvas as closing the figure will reset the canvas on + # the figure + canvas = fig.canvas plt.close() # backend is responsible for flushing any events here if plt.rcParams["backend"].lower().startswith("wx"): # TODO: debug why WX needs this only on py >= 3.8 - fig.canvas.flush_events() + canvas.flush_events() _thread_safe_backends = _get_testable_interactive_backends() From b7a5cfd99289d7a257966044479cd4f68bb7c438 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 30 Jul 2025 11:10:08 -0400 Subject: [PATCH 015/110] TST: try forcing the dpi when testing saving after closing window --- lib/matplotlib/tests/test_backends_interactive.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 0a1c7f703cad..671ad8466aee 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -220,7 +220,7 @@ def check_alt_backend(alt_backend): fig.canvas.mpl_connect("close_event", print) result = io.BytesIO() - fig.savefig(result, format='png') + fig.savefig(result, format='png', dpi=100) plt.show() @@ -230,7 +230,7 @@ def check_alt_backend(alt_backend): # When the figure is closed, it's manager is removed and the canvas is reset to # FigureCanvasBase. Saving should still be possible. result_after = io.BytesIO() - fig.savefig(result_after, format='png') + fig.savefig(result_after, format='png', dpi=100) if backend.endswith("agg"): # agg-based interactive backends should save the same image as a non-interactive From c578ec6bdf1528efe60802eedf30a6b0523ff6cc Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 30 Jul 2025 11:38:04 -0400 Subject: [PATCH 016/110] FIX: do not add super().destroy to Canvas destroy method This goes up the chain to the GUI classes that do not have this method. --- lib/matplotlib/backends/backend_gtk4.py | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index 8c9045e8cca3..cd38968779ed 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -90,7 +90,6 @@ def __init__(self, figure=None): def destroy(self): CloseEvent("close_event", self)._process() - super().destroy() def set_cursor(self, cursor): # docstring inherited From 06b97a8ff5b74ca4dc6ef7038fb66e28c1884cbd Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 30 Jul 2025 23:24:29 +0200 Subject: [PATCH 017/110] Add what's new note --- .../next_whats_new/pyplot-register-figure.rst | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 doc/users/next_whats_new/pyplot-register-figure.rst diff --git a/doc/users/next_whats_new/pyplot-register-figure.rst b/doc/users/next_whats_new/pyplot-register-figure.rst new file mode 100644 index 000000000000..9132e961e540 --- /dev/null +++ b/doc/users/next_whats_new/pyplot-register-figure.rst @@ -0,0 +1,54 @@ +Figures can be attached to and removed from pyplot +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Figures can now be attached to and removed from management through pyplot, which in +the background also means a less strict coupling to backends. + +In particular, standalone figures (created with the `.Figure` constructor) can now be +registered with the `.pyplot` module by calling ``plt.figure(fig)``. This allows to +show them with ``plt.show()`` as you would do with any figure created with pyplot +factory methods such as ``plt.figure()`` or ``plt.subplots()``. + +When closing a shown figure window, the related figure is reset to the standalone +state, i.e. it's not visible to pyplot anymore, but if you still hold a reference +to it, you can continue to work with it (e.g. do ``fig.savefig()``, or re-add it +to pyplot with ``plt.figure(fig)`` and then show it again). + +The following is now possible - though the example is exaggerated to show what's +possible. In practice, you'll stick with much simpler versions for better +consistency :: + + import matplotlib.pyplot as plt + from matplotlib.figure import Figure + + # Create a standalone figure + fig = Figure() + ax = fig.add_subplot() + ax.plot([1, 2, 3], [4, 5, 6]) + + # Register it with pyplot + plt.figure(fig) + + # Modify the figure through pyplot + plt.xlabel("x label") + + # Show the figure + plt.show() + + # Close the figure window through the GUI + + # Continue to work on the figure + fig.savefig("my_figure.png") + ax.set_ylabel("y label") + + # Re-register the figure and show it again + plt.figure(fig) + plt.show() + +Technical detail: Standalone figures use `.FigureCanvasBase` as canvas. This is +replaced by a backend-dependent subclass when registering with pyplot, and is +reset to `.FigureCanvasBase` when the figure is closed. `.Figure.savefig` uses +the current canvas to save the figure (if possible). Since `.FigureCanvasBase` +is Agg-based any Agg-based backend will create the same file output. There may +be slight differences for non-Agg backends; e.g. if you use "GTK4Cairo" as +interactive backend, ``fig.savefig("file.png")`` may create a slightly different +image depending on whether the figure is registered with pyplot or not. From b17b0623880de7b44d4b9625b34b7af33582846c Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Thu, 31 Jul 2025 14:05:00 -0500 Subject: [PATCH 018/110] Zenodo v3.10.5 --- doc/_static/zenodo_cache/16644850.svg | 35 +++++++++++++++++++++++++++ doc/project/citing.rst | 3 +++ tools/cache_zenodo_svg.py | 1 + 3 files changed, 39 insertions(+) create mode 100644 doc/_static/zenodo_cache/16644850.svg diff --git a/doc/_static/zenodo_cache/16644850.svg b/doc/_static/zenodo_cache/16644850.svg new file mode 100644 index 000000000000..89910032da4e --- /dev/null +++ b/doc/_static/zenodo_cache/16644850.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.16644850 + + + 10.5281/zenodo.16644850 + + + \ No newline at end of file diff --git a/doc/project/citing.rst b/doc/project/citing.rst index 8b4c323229ca..249f568625db 100644 --- a/doc/project/citing.rst +++ b/doc/project/citing.rst @@ -32,6 +32,9 @@ By version .. START OF AUTOGENERATED +v3.10.5 + .. image:: ../_static/zenodo_cache/16644850.svg + :target: https://doi.org/10.5281/zenodo.16644850 v3.10.3 .. image:: ../_static/zenodo_cache/15375714.svg :target: https://doi.org/10.5281/zenodo.15375714 diff --git a/tools/cache_zenodo_svg.py b/tools/cache_zenodo_svg.py index 229e90efeb34..59d6fce55162 100644 --- a/tools/cache_zenodo_svg.py +++ b/tools/cache_zenodo_svg.py @@ -63,6 +63,7 @@ def _get_xdg_cache_dir(): if __name__ == "__main__": data = { + "v3.10.5": "16644850", "v3.10.3": "15375714", "v3.10.1": "14940554", "v3.10.0": "14464227", From db30cbe4a1c45c05d295e57d8aac287fede7d75b Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 31 Jul 2025 16:34:09 -0400 Subject: [PATCH 019/110] MNT: restore the old DPI when resetting the canvas This prevent the figures from growing on hi-dpi screens. --- lib/matplotlib/figure.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 00a21343eef6..330019f0e07c 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -3003,7 +3003,12 @@ def _set_base_canvas(self): This is used upon initialization of the Figure, but also to reset the canvas when decoupling from pyplot. """ + # check if we have changed the API due to hi-dpi screens + orig_dpi = getattr(self, '_original_dpi', self._dpi) FigureCanvasBase(self) # Set self.canvas as a side-effect + # put it back to what it was + if orig_dpi != self._dpi: + self.dpi = orig_dpi def set_canvas(self, canvas): """ From 1ea856d77450d61a2498228ef896212f7faeccb4 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 31 Jul 2025 16:56:59 -0400 Subject: [PATCH 020/110] TST: verify that dpi is restored on close --- lib/matplotlib/tests/test_backend_qt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index 6e147fd14380..5bb81e5c1e2d 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -215,6 +215,10 @@ def set_device_pixel_ratio(ratio): assert qt_canvas.get_width_height() == (600, 240) assert (fig.get_size_inches() == (5, 2)).all() + # check that closing the figure restores the original dpi + plt.close(fig) + assert fig.dpi == 120 + @pytest.mark.backend('QtAgg', skip_on_importerror=True) def test_subplottool(): From 0e078a7af0a08c3d9a0ca381b33816e69e21aa00 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 1 Aug 2025 12:26:31 +0200 Subject: [PATCH 021/110] Update lib/matplotlib/figure.py Co-authored-by: Ruth Comer <10599679+rcomer@users.noreply.github.com> --- lib/matplotlib/figure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 330019f0e07c..eba873cdc221 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -3003,7 +3003,7 @@ def _set_base_canvas(self): This is used upon initialization of the Figure, but also to reset the canvas when decoupling from pyplot. """ - # check if we have changed the API due to hi-dpi screens + # check if we have changed the DPI due to hi-dpi screens orig_dpi = getattr(self, '_original_dpi', self._dpi) FigureCanvasBase(self) # Set self.canvas as a side-effect # put it back to what it was From 1784c73aba335c8eb57f9656803af0ba49957717 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sat, 2 Aug 2025 03:30:16 +0200 Subject: [PATCH 022/110] Add docs to state that you should not keep a reference to the canvas --- doc/users/next_whats_new/pyplot-register-figure.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/users/next_whats_new/pyplot-register-figure.rst b/doc/users/next_whats_new/pyplot-register-figure.rst index 9132e961e540..1acc0d0bf767 100644 --- a/doc/users/next_whats_new/pyplot-register-figure.rst +++ b/doc/users/next_whats_new/pyplot-register-figure.rst @@ -51,4 +51,8 @@ the current canvas to save the figure (if possible). Since `.FigureCanvasBase` is Agg-based any Agg-based backend will create the same file output. There may be slight differences for non-Agg backends; e.g. if you use "GTK4Cairo" as interactive backend, ``fig.savefig("file.png")`` may create a slightly different -image depending on whether the figure is registered with pyplot or not. +image depending on whether the figure is registered with pyplot or not. In +general, you should not store a reference to the canvas, but rather always +obtain it from the figure with ``fig.canvas``. This will return the current +canvas, which is either the original `.FigureCanvasBase` or a backend-dependent +subclass, depending on whether the figure is registered with pyplot or not. From 6574ed51247ce252281788de7e848bbd2d50e0b9 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Mon, 4 Aug 2025 10:19:23 +0200 Subject: [PATCH 023/110] MNT: Refactor default violin KDE estimator Move the default KDE estimator from a private definition in `violinplot()` into `violin_stats()`. This makes it easier to test and debug violin_stats() as we don't have to explicitly provide a KDE method. It also becomes logically simpler, because `violinplot()` is now only `violin_stats()` + `violin()`. --- lib/matplotlib/axes/_axes.py | 14 ++----------- lib/matplotlib/cbook.py | 38 ++++++++++++++++++++++++++++++------ lib/matplotlib/cbook.pyi | 5 ++++- 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index b71f26b76d38..eacd0515ebbf 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -8878,18 +8878,8 @@ def violinplot(self, dataset, positions=None, vert=None, .Axes.violin : Draw a violin from pre-computed statistics. boxplot : Draw a box and whisker plot. """ - - def _kde_method(X, coords): - # Unpack in case of e.g. Pandas or xarray object - X = cbook._unpack_to_numpy(X) - # fallback gracefully if the vector contains only one value - if np.all(X[0] == X): - return (X[0] == coords).astype(float) - kde = mlab.GaussianKDE(X, bw_method) - return kde.evaluate(coords) - - vpstats = cbook.violin_stats(dataset, _kde_method, points=points, - quantiles=quantiles) + vpstats = cbook.violin_stats(dataset, ("GaussianKDE", bw_method), + points=points, quantiles=quantiles) return self.violin(vpstats, positions=positions, vert=vert, orientation=orientation, widths=widths, showmeans=showmeans, showextrema=showextrema, diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index a09780965b0c..8dc05d6ccc5a 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -29,7 +29,7 @@ from numpy import VisibleDeprecationWarning import matplotlib -from matplotlib import _api, _c_internal_utils +from matplotlib import _api, _c_internal_utils, mlab class _ExceptionInfo: @@ -1430,7 +1430,7 @@ def _reshape_2D(X, name): return result -def violin_stats(X, method, points=100, quantiles=None): +def violin_stats(X, method=("GaussianKDE", "scott"), points=100, quantiles=None): """ Return a list of dictionaries of data which can be used to draw a series of violin plots. @@ -1449,11 +1449,23 @@ def violin_stats(X, method, points=100, quantiles=None): Sample data that will be used to produce the gaussian kernel density estimates. Must have 2 or fewer dimensions. - method : callable + method : (name, bw_method) or callable, The method used to calculate the kernel density estimate for each - column of data. When called via ``method(v, coords)``, it should - return a vector of the values of the KDE evaluated at the values - specified in coords. + column of data. Valid values: + + - a tuple of the form ``(name, bw_method)`` where *name* currently must + always be ``"GaussianKDE"`` and *bw_method* is the method used to + calculate the estimator bandwidth. Supported values are 'scott', + 'silverman' or a float or a callable. If a float, this will be used + directly as `!kde.factor`. If a callable, it should take a + `matplotlib.mlab.GaussianKDE` instance as its only parameter and + return a float. + + - a callable with the signature :: + + def method(data: ndarray, coords: ndarray) -> ndarray + + It should return the KDE of *data* evaluated at *coords*. points : int, default: 100 Defines the number of points to evaluate each of the gaussian kernel @@ -1481,6 +1493,20 @@ def violin_stats(X, method, points=100, quantiles=None): - max: The maximum value for this column of data. - quantiles: The quantile values for this column of data. """ + if isinstance(method, tuple): + name, bw_method = method + if name != "GaussianKDE": + raise ValueError(f"Unknown KDE method name {name!r}. The only supported " + 'named method is "GaussianKDE"') + + def _kde_method(x, coords): + # fallback gracefully if the vector contains only one value + if np.all(x[0] == x): + return (x[0] == coords).astype(float) + kde = mlab.GaussianKDE(x, bw_method) + return kde.evaluate(coords) + + method = _kde_method # List of dictionaries describing each of the violins. vpstats = [] diff --git a/lib/matplotlib/cbook.pyi b/lib/matplotlib/cbook.pyi index ad14841463e8..f7959a6fd0bb 100644 --- a/lib/matplotlib/cbook.pyi +++ b/lib/matplotlib/cbook.pyi @@ -133,7 +133,10 @@ ls_mapper_r: dict[str, str] def contiguous_regions(mask: ArrayLike) -> list[np.ndarray]: ... def is_math_text(s: str) -> bool: ... def violin_stats( - X: ArrayLike, method: Callable, points: int = ..., quantiles: ArrayLike | None = ... + X: ArrayLike, + method: tuple[Literal["GaussianKDE"], Literal["scott", "silverman"] | float | Callable] | Callable = ..., + points: int = ..., + quantiles: ArrayLike | None = ... ) -> list[dict[str, Any]]: ... def pts_to_prestep(x: ArrayLike, *args: ArrayLike) -> np.ndarray: ... def pts_to_poststep(x: ArrayLike, *args: ArrayLike) -> np.ndarray: ... From 64d0b0cef9a1b874f7799a79487d145c488be557 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 5 Aug 2025 22:47:30 +0200 Subject: [PATCH 024/110] ENH: Gracefully handle python-build-standalone ImportError with Tk. Closes #30390. --- lib/matplotlib/backends/_backend_tk.py | 30 ++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index eaf868fd8bec..d8cd3e94d104 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -22,8 +22,34 @@ TimerBase, ToolContainerBase, cursors, _Mode, MouseButton, CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) from matplotlib._pylab_helpers import Gcf -from . import _tkagg -from ._tkagg import TK_PHOTO_COMPOSITE_OVERLAY, TK_PHOTO_COMPOSITE_SET + +try: + from . import _tkagg + from ._tkagg import TK_PHOTO_COMPOSITE_OVERLAY, TK_PHOTO_COMPOSITE_SET +except ImportError as e: + # catch incompatibility of python-build-standalone with Tk + cause1 = getattr(e, '__cause__', None) + cause2 = getattr(cause1, '__cause__', None) + if (isinstance(cause1, ImportError) and + isinstance(cause2, AttributeError) and + "'_tkinter' has no attribute '__file__'" in str(cause2)): + + is_uv_python = "/uv/python" in (os.path.realpath(sys.executable)) + if is_uv_python: + raise ImportError( + "Failed to import tkagg backend. You are using a uv-installed python " + "executable, which is not compatible with Tk. " + "Please use another Python interpreter or select another backend." + ) from e + else: + raise ImportError( + "Failed to import tkagg backend. This is likely caused by using a " + "Python executable based on python-build-standalone, which is not " + "compatible with Tk. " + "Please use another Python interpreter or select another backend." + ) from e + else: + raise _log = logging.getLogger(__name__) From bc2ed8750bc93cd490bad91d0adaf04241809d15 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 21 Aug 2025 15:58:22 -0400 Subject: [PATCH 025/110] DOC: add canonical uv update command in error message --- lib/matplotlib/backends/_backend_tk.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index d8cd3e94d104..5b22ec6b5d0e 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -39,7 +39,8 @@ raise ImportError( "Failed to import tkagg backend. You are using a uv-installed python " "executable, which is not compatible with Tk. " - "Please use another Python interpreter or select another backend." + "Please update your python via: " + "`uv self update && uv python upgrade --reinstall`" ) from e else: raise ImportError( From bb60ac665d8e15e5a28741da677840eba7240bc5 Mon Sep 17 00:00:00 2001 From: Amitesh Singh Date: Fri, 22 Aug 2025 12:36:43 +0530 Subject: [PATCH 026/110] DOC: Fix typo in xlabel docstring --- lib/matplotlib/pyplot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 330ccb7d7016..4531cbd3b261 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -1341,7 +1341,7 @@ def axes( - *None*: A new full window Axes is added using ``subplot(**kwargs)``. - - 4-tuple of floats *rect* = ``(left, bottom, width, height)``. + - 4-tuple of float *rect* = ``(left, bottom, width, height)``. A new Axes is added with dimensions *rect* in normalized (0, 1) units using `~.Figure.add_axes` on the current figure. From 5054100eaf8b49cfd8d42cac9c224f9f5dd92508 Mon Sep 17 00:00:00 2001 From: Logan-Pageler <48865621+Logan-Pageler@users.noreply.github.com> Date: Fri, 22 Aug 2025 02:59:49 -0700 Subject: [PATCH 027/110] Added handling for undetermined home directory (#30454) * Added handleing for undetermined home directory * Add comment on the cause of the RuntimeError --------- Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/__init__.py | 39 ++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index dc3555fd7969..d1e575107f49 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -532,19 +532,30 @@ def _get_config_or_cache_dir(xdg_base_getter): elif sys.platform.startswith(('linux', 'freebsd')): # Only call _xdg_base_getter here so that MPLCONFIGDIR is tried first, # as _xdg_base_getter can throw. - configdir = Path(xdg_base_getter(), "matplotlib") + try: + configdir = Path(xdg_base_getter(), "matplotlib") + except RuntimeError: # raised if Path.home() is not available + pass else: - configdir = Path.home() / ".matplotlib" - # Resolve the path to handle potential issues with inaccessible symlinks. - configdir = configdir.resolve() - try: - configdir.mkdir(parents=True, exist_ok=True) - except OSError as exc: - _log.warning("mkdir -p failed for path %s: %s", configdir, exc) + try: + configdir = Path.home() / ".matplotlib" + except RuntimeError: # raised if Path.home() is not available + pass + + if configdir: + # Resolve the path to handle potential issues with inaccessible symlinks. + configdir = configdir.resolve() + try: + configdir.mkdir(parents=True, exist_ok=True) + except OSError as exc: + _log.warning("mkdir -p failed for path %s: %s", configdir, exc) + else: + if os.access(str(configdir), os.W_OK) and configdir.is_dir(): + return str(configdir) + _log.warning("%s is not a writable directory", configdir) + issue_msg = "the default path ({configdir})" else: - if os.access(str(configdir), os.W_OK) and configdir.is_dir(): - return str(configdir) - _log.warning("%s is not a writable directory", configdir) + issue_msg = "resolving the home directory" # If the config or cache directory cannot be created or is not a writable # directory, create a temporary one. try: @@ -552,17 +563,17 @@ def _get_config_or_cache_dir(xdg_base_getter): except OSError as exc: raise OSError( f"Matplotlib requires access to a writable cache directory, but there " - f"was an issue with the default path ({configdir}), and a temporary " + f"was an issue with {issue_msg}, and a temporary " f"directory could not be created; set the MPLCONFIGDIR environment " f"variable to a writable directory") from exc os.environ["MPLCONFIGDIR"] = tmpdir atexit.register(shutil.rmtree, tmpdir) _log.warning( "Matplotlib created a temporary cache directory at %s because there was " - "an issue with the default path (%s); it is highly recommended to set the " + "an issue with %s; it is highly recommended to set the " "MPLCONFIGDIR environment variable to a writable directory, in particular to " "speed up the import of Matplotlib and to better support multiprocessing.", - tmpdir, configdir) + tmpdir, issue_msg) return tmpdir From 782750e606b81c5aa8007f08767a67a20659dc49 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Fri, 22 Aug 2025 12:01:49 +0200 Subject: [PATCH 028/110] Reword what's new message Co-authored-by: Thomas A Caswell --- doc/users/next_whats_new/pyplot-register-figure.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/users/next_whats_new/pyplot-register-figure.rst b/doc/users/next_whats_new/pyplot-register-figure.rst index 1acc0d0bf767..dad2a0496a81 100644 --- a/doc/users/next_whats_new/pyplot-register-figure.rst +++ b/doc/users/next_whats_new/pyplot-register-figure.rst @@ -48,8 +48,8 @@ Technical detail: Standalone figures use `.FigureCanvasBase` as canvas. This is replaced by a backend-dependent subclass when registering with pyplot, and is reset to `.FigureCanvasBase` when the figure is closed. `.Figure.savefig` uses the current canvas to save the figure (if possible). Since `.FigureCanvasBase` -is Agg-based any Agg-based backend will create the same file output. There may -be slight differences for non-Agg backends; e.g. if you use "GTK4Cairo" as +can not render the figure, when using save fig it will fallback to `.FigureCanvasAgg` which is Agg-based. Any Agg-based backend will create the same file output, however +There may be slight differences for non-Agg backends; e.g. if you use "GTK4Cairo" as interactive backend, ``fig.savefig("file.png")`` may create a slightly different image depending on whether the figure is registered with pyplot or not. In general, you should not store a reference to the canvas, but rather always From 8b77fffb9a7c1b8d694a07e27edfb761dca57f3f Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 22 Aug 2025 09:37:34 -0400 Subject: [PATCH 029/110] DOC: move whats new to new location --- .../next_whats_new/pyplot-register-figure.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename doc/{users => release}/next_whats_new/pyplot-register-figure.rst (93%) diff --git a/doc/users/next_whats_new/pyplot-register-figure.rst b/doc/release/next_whats_new/pyplot-register-figure.rst similarity index 93% rename from doc/users/next_whats_new/pyplot-register-figure.rst rename to doc/release/next_whats_new/pyplot-register-figure.rst index dad2a0496a81..0c186fe0ecb9 100644 --- a/doc/users/next_whats_new/pyplot-register-figure.rst +++ b/doc/release/next_whats_new/pyplot-register-figure.rst @@ -48,7 +48,8 @@ Technical detail: Standalone figures use `.FigureCanvasBase` as canvas. This is replaced by a backend-dependent subclass when registering with pyplot, and is reset to `.FigureCanvasBase` when the figure is closed. `.Figure.savefig` uses the current canvas to save the figure (if possible). Since `.FigureCanvasBase` -can not render the figure, when using save fig it will fallback to `.FigureCanvasAgg` which is Agg-based. Any Agg-based backend will create the same file output, however +can not render the figure, when using savefig it will fallback to`.FigureCanvasAgg` +which is Agg-based. Any Agg-based backend will create the same file output, however There may be slight differences for non-Agg backends; e.g. if you use "GTK4Cairo" as interactive backend, ``fig.savefig("file.png")`` may create a slightly different image depending on whether the figure is registered with pyplot or not. In From 59b29f5ecf314879bd227b4e1c0012cfa6047922 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 22 Aug 2025 13:44:12 -0400 Subject: [PATCH 030/110] DOC: tweak suggestion for upgrading uv managed Python Co-authored-by: Zanie Blue --- lib/matplotlib/backends/_backend_tk.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 5b22ec6b5d0e..5f49add4fe55 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -37,10 +37,10 @@ is_uv_python = "/uv/python" in (os.path.realpath(sys.executable)) if is_uv_python: raise ImportError( - "Failed to import tkagg backend. You are using a uv-installed python " - "executable, which is not compatible with Tk. " - "Please update your python via: " - "`uv self update && uv python upgrade --reinstall`" + "Failed to import tkagg backend. You appear to be using an outdated " + "version of uv's managed Python distribution which is not compatible with Tk. " + "Please upgrade to the latest uv version, then update Python with:" + "`uv python upgrade --reinstall`" ) from e else: raise ImportError( From 4aa20caf9ea4db269902875cca841f5677fa0fd5 Mon Sep 17 00:00:00 2001 From: "Christine P. Chai" Date: Fri, 22 Aug 2025 21:27:32 -0700 Subject: [PATCH 031/110] DOC: Correct a typo: confuzzlment -> confuzzlement --- doc/devel/communication_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/devel/communication_guide.rst b/doc/devel/communication_guide.rst index e44d9368da93..c90d1d93b99d 100644 --- a/doc/devel/communication_guide.rst +++ b/doc/devel/communication_guide.rst @@ -215,7 +215,7 @@ On social media, Matplotlib: * Highlights various parts of the library, especially the more obscure bits and bobbles. * Acknowledges that it is a sometimes frustrating tangle of bits & bobbles that - can confuse even the folks who work on it & signal boosts their confuzzlment. + can confuse even the folks who work on it & signal boosts their confuzzlement. Behavior From b00e066e4c3aeefc9f59a23c5f85b5fef54772a9 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 15 Aug 2025 13:14:07 +0200 Subject: [PATCH 032/110] Use standard property alias machinery in contour(). Note that 'color(s)' remains special-cased because the ContourSet constructor needs to do a fair bit of special wrangling for it. --- lib/matplotlib/contour.py | 25 ++++++++----------------- lib/matplotlib/tests/test_contour.py | 12 ++++++++++++ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 8c5c9b566441..ffe680eaef16 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -602,7 +602,7 @@ def __init__(self, ax, *args, hatches=(None,), alpha=None, origin=None, extent=None, cmap=None, colors=None, norm=None, vmin=None, vmax=None, colorizer=None, extend='neither', antialiased=None, nchunk=0, - locator=None, transform=None, negative_linestyles=None, clip_path=None, + locator=None, transform=None, negative_linestyles=None, **kwargs): """ Draw contour lines or filled regions, depending on @@ -656,7 +656,6 @@ def __init__(self, ax, *args, super().__init__( antialiaseds=antialiased, alpha=alpha, - clip_path=clip_path, transform=transform, colorizer=colorizer, ) @@ -672,6 +671,9 @@ def __init__(self, ax, *args, self.nchunk = nchunk self.locator = locator + if "color" in kwargs: + raise _api.kwarg_error("ContourSet.__init__", "color") + if colorizer: self._set_colorizer_check_keywords(colorizer, cmap=cmap, norm=norm, vmin=vmin, @@ -764,23 +766,18 @@ def __init__(self, ax, *args, _api.warn_external('linewidths is ignored by contourf') # Lower and upper contour levels. lowers, uppers = self._get_lowers_and_uppers() - self.set( - edgecolor="none", - # Default zorder taken from Collection - zorder=kwargs.pop("zorder", 1), - rasterized=kwargs.pop("rasterized", False), - ) - + self.set(edgecolor="none") else: self.set( facecolor="none", linewidths=self._process_linewidths(linewidths), linestyle=self._process_linestyles(linestyles), + label="_nolegend_", # Default zorder taken from LineCollection, which is higher # than for filled contours so that lines are displayed on top. - zorder=kwargs.pop("zorder", 2), - label="_nolegend_", + zorder=2, ) + self.set(**kwargs) # Let user-set values override defaults. self.axes.add_collection(self, autolim=False) self.sticky_edges.x[:] = [self._mins[0], self._maxs[0]] @@ -790,12 +787,6 @@ def __init__(self, ax, *args, self.changed() # set the colors - if kwargs: - _api.warn_external( - 'The following kwargs were not used by contour: ' + - ", ".join(map(repr, kwargs)) - ) - allsegs = property(lambda self: [ [subp.vertices for subp in p._iter_connected_components()] for p in self.get_paths()]) diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index c001fc0a9191..89c1c0a6cc96 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -865,3 +865,15 @@ def test_contourf_rasterize(): circle = mpatches.Circle([0.5, 0.5], 0.5, transform=ax.transAxes) cs = ax.contourf(data, clip_path=circle, rasterized=True) assert cs._rasterized + + +@check_figures_equal(extensions=["png"]) +def test_contour_aliases(fig_test, fig_ref): + data = np.arange(100).reshape((10, 10)) ** 2 + fig_test.add_subplot().contour(data, linestyle=":") + fig_ref.add_subplot().contour(data, linestyles="dotted") + + +def test_contour_singular_color(): + with pytest.raises(TypeError): + plt.figure().add_subplot().contour([[0, 1], [2, 3]], color="r") From c332377bcc14b3826e4500a2da730d3c3552ac09 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 24 Aug 2025 03:22:47 +0200 Subject: [PATCH 033/110] Fix line length --- lib/matplotlib/backends/_backend_tk.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index 5f49add4fe55..b1d5521577d4 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -38,9 +38,9 @@ if is_uv_python: raise ImportError( "Failed to import tkagg backend. You appear to be using an outdated " - "version of uv's managed Python distribution which is not compatible with Tk. " - "Please upgrade to the latest uv version, then update Python with:" - "`uv python upgrade --reinstall`" + "version of uv's managed Python distribution which is not compatible " + " with Tk. Please upgrade to the latest uv version, then update " + " Python with: `uv python upgrade --reinstall`" ) from e else: raise ImportError( From ed12aa97ba67b673693994ca31458f6715fe1734 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 24 Aug 2025 03:23:35 +0200 Subject: [PATCH 034/110] Mentioned fix in recent python-build-standalone --- lib/matplotlib/backends/_backend_tk.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index b1d5521577d4..bec93c2b0ec6 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -46,8 +46,9 @@ raise ImportError( "Failed to import tkagg backend. This is likely caused by using a " "Python executable based on python-build-standalone, which is not " - "compatible with Tk. " - "Please use another Python interpreter or select another backend." + "compatible with Tk. Recent versions of python-build-standalone " + "should be compatible with Tk. Please update your python version " + "or select another backend." ) from e else: raise From 6d1f6d3548dbe82aa3b63007a76ec59ed63ac4de Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 24 Aug 2025 03:24:28 +0200 Subject: [PATCH 035/110] whitespace fix --- lib/matplotlib/backends/_backend_tk.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index bec93c2b0ec6..e547b88329f3 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -39,8 +39,8 @@ raise ImportError( "Failed to import tkagg backend. You appear to be using an outdated " "version of uv's managed Python distribution which is not compatible " - " with Tk. Please upgrade to the latest uv version, then update " - " Python with: `uv python upgrade --reinstall`" + "with Tk. Please upgrade to the latest uv version, then update " + "Python with: `uv python upgrade --reinstall`" ) from e else: raise ImportError( From 450db185209b57106e69bbbdf1931356001e46f7 Mon Sep 17 00:00:00 2001 From: Hannah Date: Thu, 21 Aug 2025 14:44:23 -0400 Subject: [PATCH 036/110] factored out quick install index tab to seperate file and added tab set to getting started guide --- doc/index.rst | 55 +++-------------------------- doc/install/index.rst | 7 +++- doc/install/quick_install.inc.rst | 54 ++++++++++++++++++++++++++++ doc/users/getting_started/index.rst | 21 +---------- 4 files changed, 65 insertions(+), 72 deletions(-) create mode 100644 doc/install/quick_install.inc.rst diff --git a/doc/index.rst b/doc/index.rst index e3cd7b120cd3..e77f405abb4b 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -14,59 +14,12 @@ and interactive visualizations. Install ======= -.. tab-set:: - :class: sd-width-content-min +.. include:: install/quick_install.inc.rst - .. tab-item:: pip +.. toctree:: + :hidden: - .. code-block:: bash - - pip install matplotlib - - .. tab-item:: conda - - .. code-block:: bash - - 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 - .. toctree:: - :maxdepth: 2 - - install/index - -For more detailed instructions, see the -:doc:`installation guide `. + install/index Learn ===== diff --git a/doc/install/index.rst b/doc/install/index.rst index 4eb4c201862c..21804aa81622 100644 --- a/doc/install/index.rst +++ b/doc/install/index.rst @@ -7,6 +7,9 @@ Installation ************ +.. include:: quick_install.inc.rst + +.. _install-official: Install an official release =========================== @@ -35,6 +38,7 @@ precompiled wheel for your OS and Python. animations and a larger selection of file formats, you can install :ref:`optional dependencies `. +.. _install-third-party: Third-party distributions ========================= @@ -75,7 +79,7 @@ you can install Matplotlib via your package manager, e.g.: .. redirect-from:: /users/installing/installing_source -.. _install_from_source: +.. _install-nightly-build: Install a nightly build ======================= @@ -93,6 +97,7 @@ scientific-python-nightly-wheels as the package index to query:: --extra-index-url https://pypi.org/simple \ matplotlib +.. _install-source: Install from source =================== diff --git a/doc/install/quick_install.inc.rst b/doc/install/quick_install.inc.rst new file mode 100644 index 000000000000..2e75b332f6ed --- /dev/null +++ b/doc/install/quick_install.inc.rst @@ -0,0 +1,54 @@ +.. set of quick install commands for reuse across docs + +.. tab-set:: + :class: sd-width-content-min + + .. tab-item:: pip + + .. code-block:: bash + + pip install matplotlib + + .. tab-item:: conda + + .. code-block:: bash + + 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 + + :ref:`install-official` + + :ref:`install-third-party` + + :ref:`install-nightly-build` + + :ref:`install-source` diff --git a/doc/users/getting_started/index.rst b/doc/users/getting_started/index.rst index ac896687979d..dfbbd615b5cd 100644 --- a/doc/users/getting_started/index.rst +++ b/doc/users/getting_started/index.rst @@ -4,26 +4,7 @@ Getting started Installation quick-start ------------------------ -.. grid:: 1 1 2 2 - - .. grid-item:: - - Install using `pip `__: - - .. code-block:: bash - - pip install matplotlib - - .. grid-item:: - - Install using `conda `__: - - .. code-block:: bash - - conda install -c conda-forge matplotlib - -Further details are available in the :doc:`Installation Guide `. - +.. include:: /install/quick_install.inc.rst Draw a first plot ----------------- From e44495f9f940adc5003795386c70675595908262 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 24 Aug 2025 08:26:54 +0200 Subject: [PATCH 037/110] Backport PR #30451: doc: factor out quick install tab for reuse --- doc/index.rst | 55 +++-------------------------- doc/install/index.rst | 7 +++- doc/install/quick_install.inc.rst | 54 ++++++++++++++++++++++++++++ doc/users/getting_started/index.rst | 21 +---------- 4 files changed, 65 insertions(+), 72 deletions(-) create mode 100644 doc/install/quick_install.inc.rst diff --git a/doc/index.rst b/doc/index.rst index 74a183d6cd7b..c5b25b6d37aa 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -14,59 +14,12 @@ and interactive visualizations. Install ======= -.. tab-set:: - :class: sd-width-content-min +.. include:: install/quick_install.inc.rst - .. tab-item:: pip +.. toctree:: + :hidden: - .. code-block:: bash - - pip install matplotlib - - .. tab-item:: conda - - .. code-block:: bash - - 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 - .. toctree:: - :maxdepth: 2 - - install/index - -For more detailed instructions, see the -:doc:`installation guide `. + install/index Learn ===== diff --git a/doc/install/index.rst b/doc/install/index.rst index 2d9e724e6a73..eb5e02f98868 100644 --- a/doc/install/index.rst +++ b/doc/install/index.rst @@ -5,6 +5,9 @@ Installation ************ +.. include:: quick_install.inc.rst + +.. _install-official: Install an official release =========================== @@ -35,6 +38,7 @@ precompiled wheel for your OS and Python. animations and a larger selection of file formats, you can install :ref:`optional dependencies `. +.. _install-third-party: Third-party distributions ========================= @@ -81,7 +85,7 @@ you can install Matplotlib via your package manager, e.g.: .. redirect-from:: /users/installing/installing_source -.. _install_from_source: +.. _install-nightly-build: Install a nightly build ======================= @@ -101,6 +105,7 @@ scientific-python-nightly-wheels as the package index to query: --extra-index-url https://pypi.org/simple \ matplotlib +.. _install-source: Install from source =================== diff --git a/doc/install/quick_install.inc.rst b/doc/install/quick_install.inc.rst new file mode 100644 index 000000000000..2e75b332f6ed --- /dev/null +++ b/doc/install/quick_install.inc.rst @@ -0,0 +1,54 @@ +.. set of quick install commands for reuse across docs + +.. tab-set:: + :class: sd-width-content-min + + .. tab-item:: pip + + .. code-block:: bash + + pip install matplotlib + + .. tab-item:: conda + + .. code-block:: bash + + 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 + + :ref:`install-official` + + :ref:`install-third-party` + + :ref:`install-nightly-build` + + :ref:`install-source` diff --git a/doc/users/getting_started/index.rst b/doc/users/getting_started/index.rst index ac896687979d..dfbbd615b5cd 100644 --- a/doc/users/getting_started/index.rst +++ b/doc/users/getting_started/index.rst @@ -4,26 +4,7 @@ Getting started Installation quick-start ------------------------ -.. grid:: 1 1 2 2 - - .. grid-item:: - - Install using `pip `__: - - .. code-block:: bash - - pip install matplotlib - - .. grid-item:: - - Install using `conda `__: - - .. code-block:: bash - - conda install -c conda-forge matplotlib - -Further details are available in the :doc:`Installation Guide `. - +.. include:: /install/quick_install.inc.rst Draw a first plot ----------------- From 3305482033e63ac1d7cce4418407d11164aea612 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 24 Aug 2025 08:26:54 +0200 Subject: [PATCH 038/110] Backport PR #30451: doc: factor out quick install tab for reuse --- doc/index.rst | 55 +++-------------------------- doc/install/index.rst | 7 +++- doc/install/quick_install.inc.rst | 54 ++++++++++++++++++++++++++++ doc/users/getting_started/index.rst | 21 +---------- 4 files changed, 65 insertions(+), 72 deletions(-) create mode 100644 doc/install/quick_install.inc.rst diff --git a/doc/index.rst b/doc/index.rst index 74a183d6cd7b..c5b25b6d37aa 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -14,59 +14,12 @@ and interactive visualizations. Install ======= -.. tab-set:: - :class: sd-width-content-min +.. include:: install/quick_install.inc.rst - .. tab-item:: pip +.. toctree:: + :hidden: - .. code-block:: bash - - pip install matplotlib - - .. tab-item:: conda - - .. code-block:: bash - - 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 - .. toctree:: - :maxdepth: 2 - - install/index - -For more detailed instructions, see the -:doc:`installation guide `. + install/index Learn ===== diff --git a/doc/install/index.rst b/doc/install/index.rst index 2d9e724e6a73..eb5e02f98868 100644 --- a/doc/install/index.rst +++ b/doc/install/index.rst @@ -5,6 +5,9 @@ Installation ************ +.. include:: quick_install.inc.rst + +.. _install-official: Install an official release =========================== @@ -35,6 +38,7 @@ precompiled wheel for your OS and Python. animations and a larger selection of file formats, you can install :ref:`optional dependencies `. +.. _install-third-party: Third-party distributions ========================= @@ -81,7 +85,7 @@ you can install Matplotlib via your package manager, e.g.: .. redirect-from:: /users/installing/installing_source -.. _install_from_source: +.. _install-nightly-build: Install a nightly build ======================= @@ -101,6 +105,7 @@ scientific-python-nightly-wheels as the package index to query: --extra-index-url https://pypi.org/simple \ matplotlib +.. _install-source: Install from source =================== diff --git a/doc/install/quick_install.inc.rst b/doc/install/quick_install.inc.rst new file mode 100644 index 000000000000..2e75b332f6ed --- /dev/null +++ b/doc/install/quick_install.inc.rst @@ -0,0 +1,54 @@ +.. set of quick install commands for reuse across docs + +.. tab-set:: + :class: sd-width-content-min + + .. tab-item:: pip + + .. code-block:: bash + + pip install matplotlib + + .. tab-item:: conda + + .. code-block:: bash + + 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 + + :ref:`install-official` + + :ref:`install-third-party` + + :ref:`install-nightly-build` + + :ref:`install-source` diff --git a/doc/users/getting_started/index.rst b/doc/users/getting_started/index.rst index ac896687979d..dfbbd615b5cd 100644 --- a/doc/users/getting_started/index.rst +++ b/doc/users/getting_started/index.rst @@ -4,26 +4,7 @@ Getting started Installation quick-start ------------------------ -.. grid:: 1 1 2 2 - - .. grid-item:: - - Install using `pip `__: - - .. code-block:: bash - - pip install matplotlib - - .. grid-item:: - - Install using `conda `__: - - .. code-block:: bash - - conda install -c conda-forge matplotlib - -Further details are available in the :doc:`Installation Guide `. - +.. include:: /install/quick_install.inc.rst Draw a first plot ----------------- From dab72b8386351ce509bf0848ef8440664cbc0913 Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Sat, 23 Aug 2025 22:35:01 +0100 Subject: [PATCH 039/110] DOC: simplify hat graph example --- .../lines_bars_and_markers/hat_graph.py | 40 ++++++++----------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/galleries/examples/lines_bars_and_markers/hat_graph.py b/galleries/examples/lines_bars_and_markers/hat_graph.py index 0fb611bc9262..25e5f0b1ead3 100644 --- a/galleries/examples/lines_bars_and_markers/hat_graph.py +++ b/galleries/examples/lines_bars_and_markers/hat_graph.py @@ -29,35 +29,28 @@ def hat_graph(ax, xlabels, values, group_labels): The group labels displayed in the legend. """ - def label_bars(heights, rects): - """Attach a text label on top of each bar.""" - for height, rect in zip(heights, rects): - ax.annotate(f'{height}', - xy=(rect.get_x() + rect.get_width() / 2, height), - xytext=(0, 4), # 4 points vertical offset. - textcoords='offset points', - ha='center', va='bottom') - values = np.asarray(values) - x = np.arange(values.shape[1]) - ax.set_xticks(x, labels=xlabels) - spacing = 0.3 # spacing between hat groups - width = (1 - spacing) / values.shape[0] - heights0 = values[0] - for i, (heights, group_label) in enumerate(zip(values, group_labels)): - style = {'fill': False} if i == 0 else {'edgecolor': 'black'} - rects = ax.bar(x - spacing/2 + i * width, heights - heights0, - width, bottom=heights0, label=group_label, **style) - label_bars(heights, rects) + color_cycle_colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] + + # Draw the hats + bars = ax.grouped_bar( + (values - values[0]).T, bottom=values[0], tick_labels=xlabels, + labels=group_labels, edgecolor='black', group_spacing=0.8, + colors=['none'] + color_cycle_colors) + # Attach a text label on top of each bar + for bc, heights in zip(bars.bar_containers, values): + ax.bar_label(bc, heights, padding=4) -# initialise labels and a numpy array make sure you have + +# Initialise labels and a numpy array make sure you have # N labels of N number of values in the array xlabels = ['I', 'II', 'III', 'IV', 'V'] playerA = np.array([5, 15, 22, 20, 25]) playerB = np.array([25, 32, 34, 30, 27]) -fig, ax = plt.subplots() +fig, ax = plt.subplots(layout='constrained') + hat_graph(ax, xlabels, [playerA, playerB], ['Player A', 'Player B']) # Add some text for labels, title and custom x-axis tick labels, etc. @@ -67,7 +60,6 @@ def label_bars(heights, rects): ax.set_title('Scores by number of game and players') ax.legend() -fig.tight_layout() plt.show() # %% # @@ -76,8 +68,8 @@ def label_bars(heights, rects): # The use of the following functions, methods, classes and modules is shown # in this example: # -# - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar` -# - `matplotlib.axes.Axes.annotate` / `matplotlib.pyplot.annotate` +# - `matplotlib.axes.Axes.grouped_bar` / `matplotlib.pyplot.grouped_bar` +# - `matplotlib.axes.Axes.bar_label` / `matplotlib.pyplot.bar_label` # # .. tags:: # From 36e0f1accdb62e0b2b5ea5bfa03ea35bb5d2530a Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 24 Aug 2025 23:36:14 +0200 Subject: [PATCH 040/110] FIX: Mark shared Axes as stale when propagating adjustable --- lib/matplotlib/axes/_base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index fa628b3f34e0..fd4c26d14a36 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1736,6 +1736,8 @@ def get_adjustable(self): Return whether the Axes will adjust its physical dimension ('box') or its data limits ('datalim') to achieve the desired aspect ratio. + Newly created Axes default to 'box'. + See Also -------- matplotlib.axes.Axes.set_adjustable @@ -1761,6 +1763,8 @@ def set_adjustable(self, adjustable, share=False): See Also -------- + matplotlib.axes.Axes.get_adjustable + Return the current value of *adjustable*. matplotlib.axes.Axes.set_aspect For a description of aspect handling. @@ -1792,7 +1796,7 @@ def set_adjustable(self, adjustable, share=False): "Axes which override 'get_data_ratio'") for ax in axs: ax._adjustable = adjustable - self.stale = True + ax.stale = True def get_box_aspect(self): """ From 1f2ca75ed9481793cda8227173e3622618c5a3c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Mon, 25 Aug 2025 16:59:06 +0200 Subject: [PATCH 041/110] removed test_image_cursor_formatting() This test does not test im.format_cursor_data() because it test it with 1d arrays, whereas the artist.format_cursor_data() should forward to colrizingArtist._format_cursor_data_override() but this only happens with 0d data. The correct behavior with nans is tested in the existing test test_format_cursor_data(), so the test removed here, even if working as intended, is superfluous. --- lib/matplotlib/tests/test_image.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 00c223c59362..330a2fab503d 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -1168,21 +1168,6 @@ def test_respects_bbox(): assert buf_before.getvalue() != buf_after.getvalue() # Not all white. -def test_image_cursor_formatting(): - fig, ax = plt.subplots() - # Create a dummy image to be able to call format_cursor_data - im = ax.imshow(np.zeros((4, 4))) - - data = np.ma.masked_array([0], mask=[True]) - assert im.format_cursor_data(data) == '[]' - - data = np.ma.masked_array([0], mask=[False]) - assert im.format_cursor_data(data) == '[0]' - - data = np.nan - assert im.format_cursor_data(data) == '[nan]' - - @check_figures_equal(extensions=['png', 'pdf', 'svg']) def test_image_array_alpha(fig_test, fig_ref): """Per-pixel alpha channel test.""" From 64ecd118ec2029da602bee9ee80e22d40d8317dd Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 25 Aug 2025 22:35:14 +0200 Subject: [PATCH 042/110] Let ticklabels respect set_in_layout(False). This is useful for manual finetuning of layout of static plots. --- lib/matplotlib/axis.py | 6 ++++-- lib/matplotlib/tests/test_axis.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index d80e7b4dafb9..ba17c634ac5c 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1333,9 +1333,11 @@ def _update_ticks(self): def _get_ticklabel_bboxes(self, ticks, renderer): """Return lists of bboxes for ticks' label1's and label2's.""" return ([tick.label1.get_window_extent(renderer) - for tick in ticks if tick.label1.get_visible()], + for tick in ticks + if tick.label1.get_visible() and tick.label1.get_in_layout()], [tick.label2.get_window_extent(renderer) - for tick in ticks if tick.label2.get_visible()]) + for tick in ticks + if tick.label2.get_visible() and tick.label2.get_in_layout()]) def get_tightbbox(self, renderer=None, *, for_layout_only=False): """ diff --git a/lib/matplotlib/tests/test_axis.py b/lib/matplotlib/tests/test_axis.py index 97884a33208f..67d9ed5bde62 100644 --- a/lib/matplotlib/tests/test_axis.py +++ b/lib/matplotlib/tests/test_axis.py @@ -2,6 +2,7 @@ import matplotlib.pyplot as plt from matplotlib.axis import XTick +from matplotlib.testing.decorators import check_figures_equal def test_tick_labelcolor_array(): @@ -31,6 +32,21 @@ def test_axis_not_in_layout(): assert ax1_right.get_position().bounds == ax2_right.get_position().bounds +@check_figures_equal() +def test_tick_not_in_layout(fig_test, fig_ref): + # Check that the "very long" ticklabel is ignored from layouting after + # set_in_layout(False); i.e. the layout is as if the ticklabel was empty. + # Ticklabels are set to white so that the actual string doesn't matter. + fig_test.set_layout_engine("constrained") + ax = fig_test.add_subplot(xticks=[0, 1], xticklabels=["short", "very long"]) + ax.tick_params(labelcolor="w") + fig_test.draw_without_rendering() # Ensure ticks are correct. + ax.xaxis.majorTicks[-1].label1.set_in_layout(False) + fig_ref.set_layout_engine("constrained") + ax = fig_ref.add_subplot(xticks=[0, 1], xticklabels=["short", ""]) + ax.tick_params(labelcolor="w") + + def test_translate_tick_params_reverse(): fig, ax = plt.subplots() kw = {'label1On': 'a', 'label2On': 'b', 'tick1On': 'c', 'tick2On': 'd'} From f2c2920f866788928df7945fc993ad0bc126f3a8 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 25 Aug 2025 22:39:08 +0200 Subject: [PATCH 043/110] Let triage_tests support test modules with only figure_equals tests. If there's only check_figure_equals tests in a module (e.g. test_pickle, test_axis) then there's no directory in the baseline_images test, but it still makes sense to run triage_tests in case of test failure. Previously, the missing directory caused an error; this patches fixes that. To test this, change e.g. one of the check_figures_equal tests in test_pickle to make it fail, run that test, and run tools/triage_tests.py. --- tools/triage_tests.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tools/triage_tests.py b/tools/triage_tests.py index 5153b1c712cb..6df720f29d2b 100644 --- a/tools/triage_tests.py +++ b/tools/triage_tests.py @@ -263,7 +263,7 @@ def __init__(self, path, root, source): ] self.thumbnails = [self.dir / x for x in self.thumbnails] - if not Path(self.destdir, self.generated).exists(): + if self.destdir is None or not Path(self.destdir, self.generated).exists(): # This case arises from a check_figures_equal test. self.status = 'autogen' elif ((self.dir / self.generated).read_bytes() @@ -281,7 +281,6 @@ def get_dest_dir(self, reldir): path = self.source / baseline_dir / reldir if path.is_dir(): return path - raise ValueError(f"Can't find baseline dir for {reldir}") @property def display(self): From dee6caec03ab7940be57956ded4d1cbcc5fe5bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Trygve=20Magnus=20R=C3=A6der?= Date: Mon, 25 Aug 2025 22:59:16 +0200 Subject: [PATCH 044/110] no need to senitize extrama in colorizer the @vmin.setter in Normalize (and its subclasses) call _sanitize_extrema(), so there is no reason to also do so in colorizer --- lib/matplotlib/colorizer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py index 92a6e4ea4c4f..a94790979078 100644 --- a/lib/matplotlib/colorizer.py +++ b/lib/matplotlib/colorizer.py @@ -275,9 +275,9 @@ def set_clim(self, vmin=None, vmax=None): # 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) + self.norm.vmin = vmin if vmax is not None: - self.norm.vmax = colors._sanitize_extrema(vmax) + self.norm.vmax = vmax # emit a update signal if the limits are changed if orig_vmin_vmax != (self.norm.vmin, self.norm.vmax): From 757cf5be5a0e05b11709baef275e308c8d55f195 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 25 Aug 2025 22:53:44 +0200 Subject: [PATCH 045/110] Deprecate redundant axes parameter to RadialLocator. We can just directly fetch the relevant information from `locator.axis.axes`; the case where `locator.axis.axes` was different from `axes` was not tested and doesn't make much sense anyways. The change also goes in the direction of having Locators carry less state (and hopefully, in the future, not being associated with an axis, but rather having the axis passed as argument to `__call__`). --- .../next_api_changes/deprecations/30469-AL.rst | 4 ++++ lib/matplotlib/projections/polar.py | 15 ++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/30469-AL.rst diff --git a/doc/api/next_api_changes/deprecations/30469-AL.rst b/doc/api/next_api_changes/deprecations/30469-AL.rst new file mode 100644 index 000000000000..ef3f042843c2 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30469-AL.rst @@ -0,0 +1,4 @@ +The *axes* parameter of ``RadialLocator`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... is deprecated. `~.polar.RadialLocator` now fetches the relevant information +from the Axis' parent Axes. diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 1d7c62f154f6..8b0a01f556e3 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -431,6 +431,7 @@ class RadialLocator(mticker.Locator): scale of the *r*-axis). """ + @_api.delete_parameter("3.11", "axes") def __init__(self, base, axes=None): self.base = base self._axes = axes @@ -440,11 +441,11 @@ def set_axis(self, axis): def __call__(self): # Ensure previous behaviour with full circle non-annular views. - if self._axes: - if _is_full_circle_rad(*self._axes.viewLim.intervalx): - rorigin = self._axes.get_rorigin() * self._axes.get_rsign() - if self._axes.get_rmin() <= rorigin: - return [tick for tick in self.base() if tick > rorigin] + ax = self.base.axis.axes + if _is_full_circle_rad(*ax.viewLim.intervalx): + rorigin = ax.get_rorigin() * ax.get_rsign() + if ax.get_rmin() <= rorigin: + return [tick for tick in self.base() if tick > rorigin] return self.base() def _zero_in_bounds(self): @@ -452,7 +453,7 @@ def _zero_in_bounds(self): Return True if zero is within the valid values for the scale of the radial axis. """ - vmin, vmax = self._axes.yaxis._scale.limit_range_for_scale(0, 1, 1e-5) + vmin, vmax = self.base.axis._scale.limit_range_for_scale(0, 1, 1e-5) return vmin == 0 def nonsingular(self, vmin, vmax): @@ -681,7 +682,7 @@ def __init__(self, *args, **kwargs): def set_major_locator(self, locator): if not isinstance(locator, RadialLocator): - locator = RadialLocator(locator, self.axes) + locator = RadialLocator(locator) super().set_major_locator(locator) def clear(self): From 56a7054519e8c49fba74d133435512c2238d8a16 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Tue, 26 Aug 2025 02:49:01 +0100 Subject: [PATCH 046/110] Add datetime test for ax.violin (#30384) * Add datetime test for ax.violin * Update lib/matplotlib/tests/test_datetime.py Co-authored-by: Elliott Sales de Andrade --------- Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Co-authored-by: Elliott Sales de Andrade --- lib/matplotlib/tests/test_datetime.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_datetime.py b/lib/matplotlib/tests/test_datetime.py index 821552befcf0..b3bd4d7bd151 100644 --- a/lib/matplotlib/tests/test_datetime.py +++ b/lib/matplotlib/tests/test_datetime.py @@ -810,11 +810,32 @@ def test_triplot(self): fig, ax = plt.subplots() ax.triplot(...) - @pytest.mark.xfail(reason="Test for violin not written yet") + @pytest.mark.parametrize("orientation", ["vertical", "horizontal"]) @mpl.style.context("default") - def test_violin(self): + def test_violin(self, orientation): fig, ax = plt.subplots() - ax.violin(...) + datetimes = [ + datetime.datetime(2023, 2, 10), + datetime.datetime(2023, 5, 18), + datetime.datetime(2023, 6, 6) + ] + ax.violin( + [ + { + 'coords': datetimes, + 'vals': [0.1, 0.5, 0.2], + 'mean': datetimes[1], + 'median': datetimes[1], + 'min': datetimes[0], + 'max': datetimes[-1], + 'quantiles': datetimes + } + ], + orientation=orientation, + # TODO: It should be possible for positions to be datetimes too + # https://github.com/matplotlib/matplotlib/issues/30417 + # positions=[datetime.datetime(2020, 1, 1)] + ) @pytest.mark.xfail(reason="Test for violinplot not written yet") @mpl.style.context("default") From 5db4752e1ef0e84ef137c380069165410e242a63 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 27 Aug 2025 00:47:14 -0400 Subject: [PATCH 047/110] ci: Remove cibuildwheel override for win_arm64 Our dependencies now have wheels for that platform. --- pyproject.toml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index cf8503a0f3fe..2bace246c5f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,14 +86,6 @@ local_scheme = "node-and-date" parentdir_prefix_version = "matplotlib-" fallback_version = "0.0+UNKNOWN" -# FIXME: Remove this override once dependencies are available on PyPI. -[[tool.cibuildwheel.overrides]] -select = "*-win_arm64" -before-test = """\ - pip install --pre \ - --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \ - contourpy numpy""" - [tool.isort] known_pydata = "numpy, matplotlib.pyplot" known_firstparty = "matplotlib,mpl_toolkits" From 4a260e3a169ccc4122a27082f343eb8cbad1ea7c Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 27 Aug 2025 00:58:05 -0400 Subject: [PATCH 048/110] ci: Remove cibuildwheel override for Python 3.14 testing These all have 3.14 wheels available now that the release candidate is out. --- .github/workflows/cibuildwheel.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index dcbccf97f638..9d4de069b078 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -151,10 +151,6 @@ jobs: CIBW_ENABLE: "cpython-freethreading cpython-prerelease" CIBW_ARCHS: ${{ matrix.cibw_archs }} CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 - CIBW_BEFORE_TEST: >- - python -m pip install - --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple - --upgrade --pre --only-binary=:all: contourpy numpy pillow - name: Build wheels for CPython 3.13 uses: pypa/cibuildwheel@352e01339f0a173aa2a3eb57f01492e341e83865 # v3.1.3 From eee3148571a65ae5cee4e00665d59869473d793f Mon Sep 17 00:00:00 2001 From: "Christine P. Chai" Date: Tue, 26 Aug 2025 22:38:17 -0700 Subject: [PATCH 049/110] DOC: Fix text formatting of imshow_extent example (#30471) * DOC: Make text font normal rather than italics * DOC: Update the title of imshow_extent.py --- galleries/users_explain/artists/imshow_extent.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/galleries/users_explain/artists/imshow_extent.py b/galleries/users_explain/artists/imshow_extent.py index d16a15f1e9f9..a6daa3a541c1 100644 --- a/galleries/users_explain/artists/imshow_extent.py +++ b/galleries/users_explain/artists/imshow_extent.py @@ -3,8 +3,8 @@ .. _imshow_extent: -*origin* and *extent* in `~.Axes.imshow` -======================================== +Positioning and orientation of `~.Axes.imshow` images +===================================================== :meth:`~.Axes.imshow` allows you to render an image (either a 2D array which will be color-mapped (based on *norm* and *cmap*) or a 3D RGB(A) array which From f25ceed326766d8824d2474d6125f2b6537ed18a Mon Sep 17 00:00:00 2001 From: Ian Thomas Date: Wed, 27 Aug 2025 08:57:57 +0100 Subject: [PATCH 050/110] Backport PR #30476: ci: Remove cibuildwheel override for win_arm64/Py3.14 --- .github/workflows/cibuildwheel.yml | 4 ---- pyproject.toml | 8 -------- 2 files changed, 12 deletions(-) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 4284f7037e1a..c72ab67fd43c 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -151,10 +151,6 @@ jobs: CIBW_ENABLE: "cpython-freethreading cpython-prerelease" CIBW_ARCHS: ${{ matrix.cibw_archs }} CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 - CIBW_BEFORE_TEST: >- - python -m pip install - --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple - --upgrade --pre --only-binary=:all: contourpy numpy pillow - name: Build wheels for CPython 3.13 uses: pypa/cibuildwheel@95d2f3a92fbf80abe066b09418bbf128a8923df2 # v3.0.1 diff --git a/pyproject.toml b/pyproject.toml index 5666016f0508..e6d1abaf530b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,14 +85,6 @@ local_scheme = "node-and-date" parentdir_prefix_version = "matplotlib-" fallback_version = "0.0+UNKNOWN" -# FIXME: Remove this override once dependencies are available on PyPI. -[[tool.cibuildwheel.overrides]] -select = "*-win_arm64" -before-test = """\ - pip install --pre \ - --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \ - contourpy numpy""" - [tool.isort] known_pydata = "numpy, matplotlib.pyplot" known_firstparty = "matplotlib,mpl_toolkits" From cf1a357ff9559e86ddf0933b3cd569243ebb9ec7 Mon Sep 17 00:00:00 2001 From: Ruth Comer Date: Wed, 27 Aug 2025 11:05:39 +0100 Subject: [PATCH 051/110] MNT: correct _replacer docstring [ci doc] --- lib/matplotlib/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index d1e575107f49..0b1b0e57dc6d 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1362,8 +1362,8 @@ def _init_tests(): def _replacer(data, value): """ - Either returns ``data[value]`` or passes ``data`` back, converts either to - a sequence. + Either returns ``data[value]`` or passes ``data`` back, converting any + ``MappingView`` to a sequence. """ try: # if key isn't a string don't bother From 84436f081f56e63c482c67d41c278f0d624d45f4 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 28 Aug 2025 11:50:33 +0200 Subject: [PATCH 052/110] Clarify inset_locator.inset_axes demo. ... by clearly stating that this is the docs about `inset_locator.inset_axes`, which is different from `Axes.inset_axes`. Otherwise the last sentence ("Users should consider using `Axes.inset_axes` instead") gets quite confusing. --- .../demo_colorbar_with_inset_locator.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/galleries/examples/axes_grid1/demo_colorbar_with_inset_locator.py b/galleries/examples/axes_grid1/demo_colorbar_with_inset_locator.py index f62a0f58e3bc..352c8527910e 100644 --- a/galleries/examples/axes_grid1/demo_colorbar_with_inset_locator.py +++ b/galleries/examples/axes_grid1/demo_colorbar_with_inset_locator.py @@ -1,17 +1,17 @@ """ .. _demo-colorbar-with-inset-locator: -=========================================================== -Control the position and size of a colorbar with Inset Axes -=========================================================== +========================================================================= +Control the position and size of a colorbar with inset_locator.inset_axes +========================================================================= This example shows how to control the position, height, and width of colorbars -using `~mpl_toolkits.axes_grid1.inset_locator.inset_axes`. +using `.inset_locator.inset_axes`. -Inset Axes placement is controlled as for legends: either by providing a *loc* -option ("upper right", "best", ...), or by providing a locator with respect to -the parent bbox. Parameters such as *bbox_to_anchor* and *borderpad* likewise -work in the same way, and are also demonstrated here. +`.inset_locator.inset_axes` placement is controlled as for legends: either +by providing a *loc* option ("upper right", "best", ...), or by providing a +locator with respect to the parent bbox. Parameters such as *bbox_to_anchor* +and *borderpad* likewise work in the same way, and are also demonstrated here. Users should consider using `.Axes.inset_axes` instead (see :ref:`colorbar_placement`). @@ -21,12 +21,12 @@ import matplotlib.pyplot as plt -from mpl_toolkits.axes_grid1.inset_locator import inset_axes +from mpl_toolkits.axes_grid1 import inset_locator fig, (ax1, ax2) = plt.subplots(1, 2, figsize=[6, 3]) im1 = ax1.imshow([[1, 2], [2, 3]]) -axins1 = inset_axes( +axins1 = inset_locator.inset_axes( ax1, width="50%", # width: 50% of parent_bbox width height="5%", # height: 5% @@ -36,7 +36,7 @@ fig.colorbar(im1, cax=axins1, orientation="horizontal", ticks=[1, 2, 3]) im = ax2.imshow([[1, 2], [2, 3]]) -axins = inset_axes( +axins = inset_locator.inset_axes( ax2, width="5%", # width: 5% of parent_bbox width height="50%", # height: 50% From e1e13605fd5dc7fea6589a19b9884db48498ceef Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Thu, 28 Aug 2025 13:30:11 -0500 Subject: [PATCH 053/110] Backport PR #30394: ENH: Gracefully handle python-build-standalone ImportError with Tk --- lib/matplotlib/backends/_backend_tk.py | 32 ++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index eaf868fd8bec..813e0c60620f 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -22,8 +22,36 @@ TimerBase, ToolContainerBase, cursors, _Mode, MouseButton, CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) from matplotlib._pylab_helpers import Gcf -from . import _tkagg -from ._tkagg import TK_PHOTO_COMPOSITE_OVERLAY, TK_PHOTO_COMPOSITE_SET + +try: + from . import _tkagg + from ._tkagg import TK_PHOTO_COMPOSITE_OVERLAY, TK_PHOTO_COMPOSITE_SET +except ImportError as e: + # catch incompatibility of python-build-standalone with Tk + cause1 = getattr(e, '__cause__', None) + cause2 = getattr(cause1, '__cause__', None) + if (isinstance(cause1, ImportError) and + isinstance(cause2, AttributeError) and + "'_tkinter' has no attribute '__file__'" in str(cause2)): + + is_uv_python = "/uv/python" in (os.path.realpath(sys.executable)) + if is_uv_python: + raise ImportError( + "Failed to import tkagg backend. You appear to be using an outdated " + "version of uv's managed Python distribution which is not compatible " + "with Tk. Please upgrade to the latest uv version, then update " + "Python with: `uv python upgrade --reinstall`" + ) from e + else: + raise ImportError( + "Failed to import tkagg backend. This is likely caused by using a " + "Python executable based on python-build-standalone, which is not " + "compatible with Tk. Recent versions of python-build-standalone " + "should be compatible with Tk. Please update your python version " + "or select another backend." + ) from e + else: + raise _log = logging.getLogger(__name__) From a6267f787bd3a927de7ce56456675abb3eb140e9 Mon Sep 17 00:00:00 2001 From: Brian Christian Date: Tue, 26 Aug 2025 15:40:09 -0700 Subject: [PATCH 054/110] Fix spelling error in `contains_branch_seperately` method name Fixes #30474. This PR fixes a long-standing spelling error in the `contains_branch_seperately` method name, which should be `contains_branch_separately`. In order to maintain backwards compatibility, aliases maintain the old spelling for legacy use. Type hints and internal references are updated to the new spelling accordingly. --- lib/matplotlib/axes/_axes.py | 6 +++--- lib/matplotlib/axes/_base.py | 10 +++++----- lib/matplotlib/collections.py | 2 +- lib/matplotlib/contour.py | 2 +- lib/matplotlib/tests/test_transforms.py | 4 ++-- lib/matplotlib/transforms.py | 19 ++++++++++++------- lib/matplotlib/transforms.pyi | 5 +++-- 7 files changed, 27 insertions(+), 21 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index f0bc139bdc11..6da0e925ab4f 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -1183,7 +1183,7 @@ def hlines(self, y, xmin, xmax, colors=None, linestyles='solid', if self.name == "rectilinear": datalim = lines.get_datalim(self.transData) t = lines.get_transform() - updatex, updatey = t.contains_branch_seperately(self.transData) + updatex, updatey = t.contains_branch_separately(self.transData) minx = np.nanmin(datalim.xmin) maxx = np.nanmax(datalim.xmax) miny = np.nanmin(datalim.ymin) @@ -1275,7 +1275,7 @@ def vlines(self, x, ymin, ymax, colors=None, linestyles='solid', if self.name == "rectilinear": datalim = lines.get_datalim(self.transData) t = lines.get_transform() - updatex, updatey = t.contains_branch_seperately(self.transData) + updatex, updatey = t.contains_branch_separately(self.transData) minx = np.nanmin(datalim.xmin) maxx = np.nanmax(datalim.xmax) miny = np.nanmin(datalim.ymin) @@ -6809,7 +6809,7 @@ def _update_pcolor_lims(self, collection, coords): hasattr(t, '_as_mpl_transform')): t = t._as_mpl_transform(self.axes) - if t and any(t.contains_branch_seperately(self.transData)): + if t and any(t.contains_branch_separately(self.transData)): trans_to_data = t - self.transData coords = trans_to_data.transform(coords) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index fa628b3f34e0..42f6e51786bf 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -2380,9 +2380,9 @@ def add_collection(self, collection, autolim=True): # only update the dataLim for x/y if the collection uses transData # in this direction. x_is_data, y_is_data = (collection.get_transform() - .contains_branch_seperately(self.transData)) + .contains_branch_separately(self.transData)) ox_is_data, oy_is_data = (collection.get_offset_transform() - .contains_branch_seperately(self.transData)) + .contains_branch_separately(self.transData)) self.update_datalim( points, updatex=x_is_data or ox_is_data, @@ -2451,7 +2451,7 @@ def _update_line_limits(self, line): if line_trf == self.transData: data_path = path - elif any(line_trf.contains_branch_seperately(self.transData)): + elif any(line_trf.contains_branch_separately(self.transData)): # Compute the transform from line coordinates to data coordinates. trf_to_data = line_trf - self.transData # If transData is affine we can use the cached non-affine component @@ -2474,7 +2474,7 @@ def _update_line_limits(self, line): if not data_path.vertices.size: return - updatex, updatey = line_trf.contains_branch_seperately(self.transData) + updatex, updatey = line_trf.contains_branch_separately(self.transData) if self.name != "rectilinear": # This block is mostly intended to handle axvline in polar plots, # for which updatey would otherwise be True. @@ -2527,7 +2527,7 @@ def _update_patch_limits(self, patch): vertices = np.vstack(vertices) patch_trf = patch.get_transform() - updatex, updatey = patch_trf.contains_branch_seperately(self.transData) + updatex, updatey = patch_trf.contains_branch_separately(self.transData) if not (updatex or updatey): return if self.name != "rectilinear": diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index ec6d40805da0..684e15cdf854 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -283,7 +283,7 @@ def get_datalim(self, transData): offsets = self.get_offsets() - if any(transform.contains_branch_seperately(transData)): + if any(transform.contains_branch_separately(transData)): # collections that are just in data units (like quiver) # can properly have the axes limits set by their shape + # offset. LineCollections that have no offsets can diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 643bfce4273a..1125b9c5fbc6 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -1333,7 +1333,7 @@ def _process_args(self, *args, corner_mask=None, algorithm=None, **kwargs): # if the transform is not trans data, and some part of it # contains transData, transform the xs and ys to data coordinates if (t != self.axes.transData and - any(t.contains_branch_seperately(self.axes.transData))): + any(t.contains_branch_separately(self.axes.transData))): trans_to_data = t - self.axes.transData pts = np.vstack([x.flat, y.flat]).T transformed_pts = trans_to_data.transform(pts) diff --git a/lib/matplotlib/tests/test_transforms.py b/lib/matplotlib/tests/test_transforms.py index b4db34db5a91..dbff0e1ba39f 100644 --- a/lib/matplotlib/tests/test_transforms.py +++ b/lib/matplotlib/tests/test_transforms.py @@ -694,9 +694,9 @@ def test_contains_branch(self): assert not self.stack1.contains_branch(self.tn1 + self.ta2) blend = mtransforms.BlendedGenericTransform(self.tn2, self.stack2) - x, y = blend.contains_branch_seperately(self.stack2_subset) + x, y = blend.contains_branch_separately(self.stack2_subset) stack_blend = self.tn3 + blend - sx, sy = stack_blend.contains_branch_seperately(self.stack2_subset) + sx, sy = stack_blend.contains_branch_separately(self.stack2_subset) assert x is sx is False assert y is sy is True diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 350113c56170..a22a2f7db070 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -1418,7 +1418,7 @@ def contains_branch(self, other): return True return False - def contains_branch_seperately(self, other_transform): + def contains_branch_separately(self, other_transform): """ Return whether the given branch is a sub-tree of this transform on each separate dimension. @@ -1426,16 +1426,21 @@ def contains_branch_seperately(self, other_transform): A common use for this method is to identify if a transform is a blended transform containing an Axes' data transform. e.g.:: - x_isdata, y_isdata = trans.contains_branch_seperately(ax.transData) + x_isdata, y_isdata = trans.contains_branch_separately(ax.transData) """ if self.output_dims != 2: - raise ValueError('contains_branch_seperately only supports ' + raise ValueError('contains_branch_separately only supports ' 'transforms with 2 output dimensions') # for a non-blended transform each separate dimension is the same, so # just return the appropriate shape. return (self.contains_branch(other_transform), ) * 2 + # Permanent alias for backwards compatibility (historical typo) + def contains_branch_seperately(self, other_transform): + """:meta private:""" + return self.contains_branch_separately(other_transform) + def __sub__(self, other): """ Compose *self* with the inverse of *other*, cancelling identical terms @@ -2185,7 +2190,7 @@ def __eq__(self, other): else: return NotImplemented - def contains_branch_seperately(self, transform): + def contains_branch_separately(self, transform): return (self._x.contains_branch(transform), self._y.contains_branch(transform)) @@ -2411,14 +2416,14 @@ def _iter_break_from_left_to_right(self): for left, right in self._b._iter_break_from_left_to_right(): yield self._a + left, right - def contains_branch_seperately(self, other_transform): + def contains_branch_separately(self, other_transform): # docstring inherited if self.output_dims != 2: - raise ValueError('contains_branch_seperately only supports ' + raise ValueError('contains_branch_separately only supports ' 'transforms with 2 output dimensions') if self == other_transform: return (True, True) - return self._b.contains_branch_seperately(other_transform) + return self._b.contains_branch_separately(other_transform) depth = property(lambda self: self._a.depth + self._b.depth) is_affine = property(lambda self: self._a.is_affine and self._b.is_affine) diff --git a/lib/matplotlib/transforms.pyi b/lib/matplotlib/transforms.pyi index 07d299be297c..4e582904d1d8 100644 --- a/lib/matplotlib/transforms.pyi +++ b/lib/matplotlib/transforms.pyi @@ -189,9 +189,10 @@ class Transform(TransformNode): @property def depth(self) -> int: ... def contains_branch(self, other: Transform) -> bool: ... - def contains_branch_seperately( + def contains_branch_separately( self, other_transform: Transform ) -> Sequence[bool]: ... + contains_branch_seperately = contains_branch_separately # Alias (historical typo) def __sub__(self, other: Transform) -> Transform: ... def __array__(self, *args, **kwargs) -> np.ndarray: ... def transform(self, values: ArrayLike) -> np.ndarray: ... @@ -252,7 +253,7 @@ class IdentityTransform(Affine2DBase): ... class _BlendedMixin: def __eq__(self, other: object) -> bool: ... - def contains_branch_seperately(self, transform: Transform) -> Sequence[bool]: ... + def contains_branch_separately(self, transform: Transform) -> Sequence[bool]: ... class BlendedGenericTransform(_BlendedMixin, Transform): input_dims: Literal[2] From 4b0a26a9d18b8a39ecbbb23d12c8ff35a5cfff41 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 28 Aug 2025 23:21:30 -0400 Subject: [PATCH 055/110] FIX: be more cautious about checking widget size This can fail if the c++ side of the object has already been cleaned up but the Python side is still lingering (due to refs in callbacks). closes #29618 --- lib/matplotlib/backends/backend_qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index dd614e516de5..d0aded5fff63 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -517,7 +517,7 @@ def _draw_idle(self): if not self._draw_pending: return self._draw_pending = False - if self.height() <= 0 or self.width() <= 0: + if _isdeleted(self) or self.height() <= 0 or self.width() <= 0: return try: self.draw() From d2b3e9596991387d46e72f021cd1701d21f51187 Mon Sep 17 00:00:00 2001 From: Geoffrey Thomas Date: Fri, 29 Aug 2025 13:39:45 -0400 Subject: [PATCH 056/110] doc: Update warnings about python-build-standalone (fixes #30409) --- doc/install/index.rst | 19 ++++++++++++++----- doc/install/quick_install.inc.rst | 17 ++++++++--------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/doc/install/index.rst b/doc/install/index.rst index 21804aa81622..4058b0549738 100644 --- a/doc/install/index.rst +++ b/doc/install/index.rst @@ -28,11 +28,20 @@ precompiled wheel for your OS and Python. .. note:: - The following backends work out of the box: Agg, ps, pdf, svg - - Python is typically shipped with tk bindings which are used by - TkAgg. Notably, python-build-standalone – used by ``uv`` – does - not include tk bindings that are usable by Matplotlib. + The following non-interactive backends work out of the box: Agg, + ps, pdf, svg + + The TkAgg interactive backend also typically works out of the box. + It requires Tk bindings, which are usually provided via the Python + standard library's ``tkinter`` module. On some OSes, you may need + to install a separate package like ``python3-tk`` to add this + component of the standard library. + + Some tools like ``uv`` make use of Python builds from the + python-build-standalone project, which only gained usable Tk + bindings recently (August 2025). If you are having trouble with the + TkAgg backend, ensure you have an up-to-date build, e.g. ``uv self + update && uv python upgrade --reinstall``. For support of other GUI frameworks, LaTeX rendering, saving animations and a larger selection of file formats, you can diff --git a/doc/install/quick_install.inc.rst b/doc/install/quick_install.inc.rst index 2e75b332f6ed..0604a3c8fe75 100644 --- a/doc/install/quick_install.inc.rst +++ b/doc/install/quick_install.inc.rst @@ -29,20 +29,19 @@ .. 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. + uv usually installs its own versions of Python from the + python-build-standalone project, and only recent versions of those + Python builds (August 2025) work properly with the ``tkagg`` backend + for displaying plots in a window. Please make sure you are using uv + 0.8.7 or newer (update with e.g. ``uv self update``) and that your + bundled Python installs are up to date (with ``uv python upgrade + --reinstall``). Alternatively, you can use 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 :ref:`install-official` From 8693a2018bac679e951c3aa70ca208cb38905b70 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 29 Aug 2025 14:12:31 -0400 Subject: [PATCH 057/110] CI: try skipping just released version of coverage --- requirements/testing/all.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/testing/all.txt b/requirements/testing/all.txt index e386924a9b67..7351c2470f9f 100644 --- a/requirements/testing/all.txt +++ b/requirements/testing/all.txt @@ -2,7 +2,7 @@ black<24 certifi -coverage!=6.3 +coverage!=6.3,!=7.10.6 psutil pytest!=4.6.0,!=5.4.0,!=8.1.0 pytest-cov From 63842c09648577d870b5ed19df383ac977b91cd8 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 29 Aug 2025 14:30:04 -0400 Subject: [PATCH 058/110] CI: try pinning back pytest-rerunfailures instead --- requirements/testing/all.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/testing/all.txt b/requirements/testing/all.txt index 7351c2470f9f..a41073bdf47e 100644 --- a/requirements/testing/all.txt +++ b/requirements/testing/all.txt @@ -2,11 +2,11 @@ black<24 certifi -coverage!=6.3,!=7.10.6 +coverage!=6.3 psutil pytest!=4.6.0,!=5.4.0,!=8.1.0 pytest-cov -pytest-rerunfailures +pytest-rerunfailures!=16.0 pytest-timeout pytest-xdist pytest-xvfb From a55b817f019c0b2a6550fa8d255a821605651ed7 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 29 Aug 2025 15:20:22 -0500 Subject: [PATCH 059/110] Backport PR #30484: FIX: be more cautious about checking widget size --- lib/matplotlib/backends/backend_qt.py | 2 +- requirements/testing/all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index dd614e516de5..d0aded5fff63 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -517,7 +517,7 @@ def _draw_idle(self): if not self._draw_pending: return self._draw_pending = False - if self.height() <= 0 or self.width() <= 0: + if _isdeleted(self) or self.height() <= 0 or self.width() <= 0: return try: self.draw() diff --git a/requirements/testing/all.txt b/requirements/testing/all.txt index e386924a9b67..a41073bdf47e 100644 --- a/requirements/testing/all.txt +++ b/requirements/testing/all.txt @@ -6,7 +6,7 @@ coverage!=6.3 psutil pytest!=4.6.0,!=5.4.0,!=8.1.0 pytest-cov -pytest-rerunfailures +pytest-rerunfailures!=16.0 pytest-timeout pytest-xdist pytest-xvfb From 4b794954750f7a1218c9d0c032ec30ba7a6088e6 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 29 Aug 2025 17:30:11 -0400 Subject: [PATCH 060/110] Backport PR #30486: doc: Update warnings about python-build-standalone --- doc/install/index.rst | 19 ++++++++++++++----- doc/install/quick_install.inc.rst | 17 ++++++++--------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/doc/install/index.rst b/doc/install/index.rst index eb5e02f98868..429e4bdaf60d 100644 --- a/doc/install/index.rst +++ b/doc/install/index.rst @@ -28,11 +28,20 @@ precompiled wheel for your OS and Python. .. note:: - The following backends work out of the box: Agg, ps, pdf, svg - - Python is typically shipped with tk bindings which are used by - TkAgg. Notably, python-build-standalone – used by ``uv`` – does - not include tk bindings that are usable by Matplotlib. + The following non-interactive backends work out of the box: Agg, + ps, pdf, svg + + The TkAgg interactive backend also typically works out of the box. + It requires Tk bindings, which are usually provided via the Python + standard library's ``tkinter`` module. On some OSes, you may need + to install a separate package like ``python3-tk`` to add this + component of the standard library. + + Some tools like ``uv`` make use of Python builds from the + python-build-standalone project, which only gained usable Tk + bindings recently (August 2025). If you are having trouble with the + TkAgg backend, ensure you have an up-to-date build, e.g. ``uv self + update && uv python upgrade --reinstall``. For support of other GUI frameworks, LaTeX rendering, saving animations and a larger selection of file formats, you can diff --git a/doc/install/quick_install.inc.rst b/doc/install/quick_install.inc.rst index 2e75b332f6ed..0604a3c8fe75 100644 --- a/doc/install/quick_install.inc.rst +++ b/doc/install/quick_install.inc.rst @@ -29,20 +29,19 @@ .. 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. + uv usually installs its own versions of Python from the + python-build-standalone project, and only recent versions of those + Python builds (August 2025) work properly with the ``tkagg`` backend + for displaying plots in a window. Please make sure you are using uv + 0.8.7 or newer (update with e.g. ``uv self update``) and that your + bundled Python installs are up to date (with ``uv python upgrade + --reinstall``). Alternatively, you can use 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 :ref:`install-official` From 25a97fd721edeee7ef2916cdbb79e050187f3d11 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Fri, 29 Aug 2025 17:30:11 -0400 Subject: [PATCH 061/110] Backport PR #30486: doc: Update warnings about python-build-standalone --- doc/install/index.rst | 19 ++++++++++++++----- doc/install/quick_install.inc.rst | 17 ++++++++--------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/doc/install/index.rst b/doc/install/index.rst index eb5e02f98868..429e4bdaf60d 100644 --- a/doc/install/index.rst +++ b/doc/install/index.rst @@ -28,11 +28,20 @@ precompiled wheel for your OS and Python. .. note:: - The following backends work out of the box: Agg, ps, pdf, svg - - Python is typically shipped with tk bindings which are used by - TkAgg. Notably, python-build-standalone – used by ``uv`` – does - not include tk bindings that are usable by Matplotlib. + The following non-interactive backends work out of the box: Agg, + ps, pdf, svg + + The TkAgg interactive backend also typically works out of the box. + It requires Tk bindings, which are usually provided via the Python + standard library's ``tkinter`` module. On some OSes, you may need + to install a separate package like ``python3-tk`` to add this + component of the standard library. + + Some tools like ``uv`` make use of Python builds from the + python-build-standalone project, which only gained usable Tk + bindings recently (August 2025). If you are having trouble with the + TkAgg backend, ensure you have an up-to-date build, e.g. ``uv self + update && uv python upgrade --reinstall``. For support of other GUI frameworks, LaTeX rendering, saving animations and a larger selection of file formats, you can diff --git a/doc/install/quick_install.inc.rst b/doc/install/quick_install.inc.rst index 2e75b332f6ed..0604a3c8fe75 100644 --- a/doc/install/quick_install.inc.rst +++ b/doc/install/quick_install.inc.rst @@ -29,20 +29,19 @@ .. 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. + uv usually installs its own versions of Python from the + python-build-standalone project, and only recent versions of those + Python builds (August 2025) work properly with the ``tkagg`` backend + for displaying plots in a window. Please make sure you are using uv + 0.8.7 or newer (update with e.g. ``uv self update``) and that your + bundled Python installs are up to date (with ``uv python upgrade + --reinstall``). Alternatively, you can use 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 :ref:`install-official` From d3365af7e461b3c4bebb0351a06809ad503a3db9 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 29 Aug 2025 18:05:48 -0500 Subject: [PATCH 062/110] Github stats v3.10.6 --- doc/users/github_stats.rst | 130 ++++------------ .../prev_whats_new/github_stats_3.10.5.rst | 142 ++++++++++++++++++ 2 files changed, 174 insertions(+), 98 deletions(-) create mode 100644 doc/users/prev_whats_new/github_stats_3.10.5.rst diff --git a/doc/users/github_stats.rst b/doc/users/github_stats.rst index e28b39088994..84c2cc5867fd 100644 --- a/doc/users/github_stats.rst +++ b/doc/users/github_stats.rst @@ -1,33 +1,29 @@ .. _github-stats: -GitHub statistics for 3.10.5 (Jul 31, 2025) +GitHub statistics for 3.10.6 (Aug 29, 2025) =========================================== -GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2025/07/31 +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2025/08/29 These lists are automatically generated, and may be incomplete or contain duplicates. -We closed 18 issues and merged 67 pull requests. -The full list can be seen `on GitHub `__ +We closed 4 issues and merged 19 pull requests. +The full list can be seen `on GitHub `__ -The following 36 authors contributed 371 commits. +The following 31 authors contributed 380 commits. +* Alan Burlot * Antony Lee -* Brian Christian -* chrisjbillington * Christine P. Chai -* Clément Robert * David Stansby * dependabot[bot] +* Doron Behar * Elliott Sales de Andrade * G.D. McBain * Greg Lucas * hannah * hu-xiaonan * Ian Thomas -* ianlv -* IdiotCoffee -* Ines Cachola * Inês Cachola * Jody Klymak * Jouni K. Seppänen @@ -41,7 +37,6 @@ The following 36 authors contributed 371 commits. * Qian Zhang * Raphael Erik Hviding * Roman -* Roman A * Ruth Comer * saikarna913 * Scott Shambaugh @@ -51,95 +46,34 @@ The following 36 authors contributed 371 commits. GitHub issues and pull requests: -Pull Requests (67): +Pull Requests (19): -* :ghpull:`30357`: CIBW updates: fix pypy sections, update cibw version -* :ghpull:`30356`: Manual Backport PR #30195 on branch v3.10.x (ci: Enable wheel builds on Python 3.14) -* :ghpull:`30352`: Backport PR #28554 on branch v3.10.x (BLD: Enable wheels on Windows-on-ARM) -* :ghpull:`30353`: Backport PR #30345 on branch v3.10.x (qt: Use better devicePixelRatio event to refresh scaling) -* :ghpull:`30350`: Backport PR #30344 on branch v3.10.x (Support fractional HiDPI in GTK4 backend) -* :ghpull:`30277`: Backport PR #30271 on branch v3.10.x (Reduce pause time in interactive timer test) -* :ghpull:`30351`: Backport PR #30327 on branch v3.10.x (FIX Update Axes limits from Axes.add_collection(... autolim=True)) -* :ghpull:`30345`: qt: Use better devicePixelRatio event to refresh scaling -* :ghpull:`28554`: BLD: Enable wheels on Windows-on-ARM -* :ghpull:`30292`: Backport PR #30237: Add explicit ``**options: Any`` for ``add_subplot`` m… -* :ghpull:`29935`: Backport PR #29908 on branch v3.10.x (TST: Use text placeholders for empty legends) -* :ghpull:`30327`: FIX Update Axes limits from Axes.add_collection(... autolim=True) -* :ghpull:`30344`: Support fractional HiDPI in GTK4 backend -* :ghpull:`30326`: Backport PR #30321 on branch v3.10.x (Fix type annotation for Axes.get_legend() to include None) -* :ghpull:`30321`: Fix type annotation for Axes.get_legend() to include None -* :ghpull:`30287`: Backport PR #30286 on branch v3.10.x (Fix whitespace in _axes.py error message) -* :ghpull:`30288`: Backport PR #30283 on branch v3.10.x (changed the FAQ link to point to the correct path) -* :ghpull:`30293`: Backport PR #30289 on branch v3.10.x (DOC: Fix build with pybind11 3) -* :ghpull:`30283`: changed the FAQ link to point to the correct path -* :ghpull:`30286`: Fix whitespace in _axes.py error message -* :ghpull:`30271`: Reduce pause time in interactive timer test -* :ghpull:`30269`: Backport PR #30186 on branch v3.10.x (Fix figure legend when drawing stackplots) -* :ghpull:`30186`: Fix figure legend when drawing stackplots -* :ghpull:`30268`: Backport PR #30233 on branch v3.10.x (Check that stem input is 1D) -* :ghpull:`30233`: Check that stem input is 1D -* :ghpull:`30259`: Backport PR #30256 on branch v3.10.x (Time out in _get_executable_info) -* :ghpull:`30256`: Time out in _get_executable_info -* :ghpull:`30237`: Add explicit ``**options: Any`` for ``add_subplot`` method -* :ghpull:`30253`: Backport PR #30243 on branch v3.10.x (Fix FancyArrow rendering for zero-length arrows) -* :ghpull:`30243`: Fix FancyArrow rendering for zero-length arrows -* :ghpull:`30250`: Backport PR #30244 on branch v3.10.x (DOC: Recommend to use bare Figure instances for saving to file) -* :ghpull:`30247`: Backport PR #30246 on branch v3.10.x (chore: remove redundant words in comment) -* :ghpull:`30246`: chore: remove redundant words in comment -* :ghpull:`30240`: Backport PR #30236 on branch v3.10.x (Copy-edit the docstring of AuxTransformBox.) -* :ghpull:`30236`: Copy-edit the docstring of AuxTransformBox. -* :ghpull:`30234`: Backport PR #30209 on branch v3.10.x (Clean up Qt socket notifier to avoid spurious interrupt handler calls) -* :ghpull:`30209`: Clean up Qt socket notifier to avoid spurious interrupt handler calls -* :ghpull:`30195`: ci: Enable wheel builds on Python 3.14 -* :ghpull:`30229`: Backport PR #30221 on branch v3.10.x (BUG: fix future incompatibility with Pillow 13) -* :ghpull:`30221`: BUG: fix future incompatibility with Pillow 13 -* :ghpull:`30228`: Backport PR #30098 on branch v3.10.x (Fix label_outer in the presence of colorbars.) -* :ghpull:`30227`: Backport PR #30223 on branch v3.10.x (Polar log scale: fix inner patch boundary and spine location) -* :ghpull:`30098`: Fix label_outer in the presence of colorbars. -* :ghpull:`30223`: Polar log scale: fix inner patch boundary and spine location -* :ghpull:`30217`: Backport PR #30198 on branch v3.10.x (Implement Path.__deepcopy__ avoiding infinite recursion) -* :ghpull:`30198`: Implement Path.__deepcopy__ avoiding infinite recursion -* :ghpull:`30213`: Backport PR #30212 on branch v3.10.x ([Doc]: fix bug in release notes for matplotlib v3.5.0 and v3.7.0) -* :ghpull:`30189`: Backport PR #30180 on branch v3.10.x (DOC: expand polar example) -* :ghpull:`30167`: Backport PR #30162 on branch v3.10.x (TST: Fix runtime error checking NaN input to format_cursor_data) -* :ghpull:`30162`: TST: Fix runtime error checking NaN input to format_cursor_data -* :ghpull:`30146`: Backport PR #30144 on branch v3.10.x (js: Fix externally-controlled format strings) -* :ghpull:`30144`: js: Fix externally-controlled format strings -* :ghpull:`30140`: Backport PR #30118 on branch v3.10.x (CI: Skip jobs on forks) -* :ghpull:`30120`: Backport PR #30114 on branch v3.10.x (Fix _is_tensorflow_array.) -* :ghpull:`30122`: Backport PR #30119 on branch v3.10.x (Add some types to _mathtext.py) -* :ghpull:`30119`: Add some types to _mathtext.py -* :ghpull:`30114`: Fix _is_tensorflow_array. -* :ghpull:`30106`: Backport PR #30089 on branch v3.10.x (FIX: fix submerged margins algorithm being applied twice) -* :ghpull:`30089`: FIX: fix submerged margins algorithm being applied twice -* :ghpull:`30101`: Backport PR #30096 on branch v3.10.x (Fix OffsetBox custom picker) -* :ghpull:`30096`: Fix OffsetBox custom picker -* :ghpull:`30081`: Backport PR #30079 on branch v3.10.x (FIX: cast legend handles to list) -* :ghpull:`30079`: FIX: cast legend handles to list -* :ghpull:`30057`: Backport PR #29895 on branch v3.10.x (The 'lines.markeredgecolor' now doesn't interfere on the color of errorbar caps)" -* :ghpull:`29895`: The 'lines.markeredgecolor' now doesn't interfere on the color of errorbar caps -* :ghpull:`30033`: Backport PR #30029 on branch v3.10.x (Update diagram in subplots_adjust documentation to clarify parameters) +* :ghpull:`30487`: Backport PR #30484 on branch v3.10.x (FIX: be more cautious about checking widget size) +* :ghpull:`30484`: FIX: be more cautious about checking widget size +* :ghpull:`30481`: Backport PR #30394 on branch v3.10.x (ENH: Gracefully handle python-build-standalone ImportError with Tk) +* :ghpull:`30477`: Backport PR #30476 on branch v3.10.x (ci: Remove cibuildwheel override for win_arm64/Py3.14) +* :ghpull:`30394`: ENH: Gracefully handle python-build-standalone ImportError with Tk +* :ghpull:`30476`: ci: Remove cibuildwheel override for win_arm64/Py3.14 +* :ghpull:`30461`: Backport PR #30451 on branch v3.10.x (doc: factor out quick install tab for reuse) +* :ghpull:`30448`: Backport PR #30412 on branch v3.10.x ({Check,Radio}Buttons: Improve docs of label_props) +* :ghpull:`30412`: {Check,Radio}Buttons: Improve docs of label_props +* :ghpull:`30445`: Backport PR #30444 on branch v3.10.x (Small correction of a typo in the galleries: axis instead of axes) +* :ghpull:`30444`: Small correction of a typo in the galleries: axis instead of axes +* :ghpull:`30430`: Backport PR #30426 on branch v3.10.x (Fix a race condition in TexManager.make_dvi.) +* :ghpull:`30434`: Backport PR #30426: Fix a race condition in TexManager.make_dvi & make_png. +* :ghpull:`30431`: Use pathlib in texmanager. +* :ghpull:`30428`: Backport PR #30399 on branch v3.10.x (Qt: Fix HiDPI handling on X11/Windows) +* :ghpull:`30426`: Fix a race condition in TexManager.make_dvi. +* :ghpull:`30399`: Qt: Fix HiDPI handling on X11/Windows +* :ghpull:`30415`: Backport PR #30414 on branch v3.10.x (DOC: update Cartopy url) +* :ghpull:`30414`: DOC: update Cartopy url -Issues (18): +Issues (4): -* :ghissue:`30370`: [Bug]: matplotlib simple example fails in Python 3.14 -* :ghissue:`30218`: [Bug]: Rendering on Wayland with fractional scaling looks bad -* :ghissue:`30318`: [Bug]: type annotation of ``Axes.get_legend()`` misses ``None`` -* :ghissue:`30169`: [Doc]: Incorrect FAQ Link on Tutorials Page -* :ghissue:`30285`: [Bug]: Missing whitespace in _axes.py error message -* :ghissue:`30280`: [Bug]: Pillow 11.3 raises a deprecation warning when using TkAgg -* :ghissue:`30158`: [Bug]: Stackplot in SubFigure raises when drawing Legend -* :ghissue:`30216`: [Bug]: stem complaining about PyTorch's Tensor -* :ghissue:`30242`: [Bug]: Cannot create empty FancyArrow (expired numpy deprecation) -* :ghissue:`30249`: [Bug]: DeprecationWarning from Pillow 11.3.0 about 'mode' parameter of PIL.Image.fromarray() -* :ghissue:`29688`: [Bug]: "Bad file descriptor" raised repeatedly when plt.pause() interrupted in IPython -* :ghissue:`27305`: [Bug]: Axes.label_outer() does not work when there is a colorbar -* :ghissue:`30179`: [Bug]: Inner border is not rendered correctly when using log-scale and polar projection. -* :ghissue:`29157`: FUTURE BUG: reconsider how we deep-copy path objects -* :ghissue:`30152`: [Bug]: Test pipeline failure on windows -* :ghissue:`30076`: [Bug]: Layout Managers are confused by complex arrangement of sub-figures and gridspec's -* :ghissue:`30078`: [Bug]: legend no longer works with itertools.chain -* :ghissue:`29780`: [Bug]: Setting 'lines.markeredgecolor' affects color of errorbar caps. +* :ghissue:`29618`: [Bug]: FigureCanvasQT is seemingly prematurely freed under certain conditions +* :ghissue:`30390`: [ENH]: Gracefully handle python-build-standalone ImportError +* :ghissue:`30420`: [ENH]: Support parallel plotting +* :ghissue:`30386`: BUG: Qt hi-dpi regression on windows and X11 with mpl 3.10.5 Previous GitHub statistics diff --git a/doc/users/prev_whats_new/github_stats_3.10.5.rst b/doc/users/prev_whats_new/github_stats_3.10.5.rst new file mode 100644 index 000000000000..319086baebe5 --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.10.5.rst @@ -0,0 +1,142 @@ +.. _github-stats-3_10_5: + +GitHub statistics for 3.10.5 (Jul 31, 2025) +=========================================== + +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2025/07/31 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 18 issues and merged 67 pull requests. +The full list can be seen `on GitHub `__ + +The following 36 authors contributed 371 commits. + +* Antony Lee +* Brian Christian +* chrisjbillington +* Christine P. Chai +* Clément Robert +* David Stansby +* dependabot[bot] +* Elliott Sales de Andrade +* G.D. McBain +* Greg Lucas +* hannah +* hu-xiaonan +* Ian Thomas +* ianlv +* IdiotCoffee +* Ines Cachola +* Inês Cachola +* Jody Klymak +* Jouni K. Seppänen +* Khushi_29 +* Kyle Sunden +* Lumberbot (aka Jack) +* N R Navaneet +* Nathan G. Wiseman +* Oscar Gustafsson +* Praful Gulani +* Qian Zhang +* Raphael Erik Hviding +* Roman +* Roman A +* Ruth Comer +* saikarna913 +* Scott Shambaugh +* Thomas A Caswell +* Tim Hoffmann +* Trygve Magnus Ræder + +GitHub issues and pull requests: + +Pull Requests (67): + +* :ghpull:`30357`: CIBW updates: fix pypy sections, update cibw version +* :ghpull:`30356`: Manual Backport PR #30195 on branch v3.10.x (ci: Enable wheel builds on Python 3.14) +* :ghpull:`30352`: Backport PR #28554 on branch v3.10.x (BLD: Enable wheels on Windows-on-ARM) +* :ghpull:`30353`: Backport PR #30345 on branch v3.10.x (qt: Use better devicePixelRatio event to refresh scaling) +* :ghpull:`30350`: Backport PR #30344 on branch v3.10.x (Support fractional HiDPI in GTK4 backend) +* :ghpull:`30277`: Backport PR #30271 on branch v3.10.x (Reduce pause time in interactive timer test) +* :ghpull:`30351`: Backport PR #30327 on branch v3.10.x (FIX Update Axes limits from Axes.add_collection(... autolim=True)) +* :ghpull:`30345`: qt: Use better devicePixelRatio event to refresh scaling +* :ghpull:`28554`: BLD: Enable wheels on Windows-on-ARM +* :ghpull:`30292`: Backport PR #30237: Add explicit ``**options: Any`` for ``add_subplot`` m… +* :ghpull:`29935`: Backport PR #29908 on branch v3.10.x (TST: Use text placeholders for empty legends) +* :ghpull:`30327`: FIX Update Axes limits from Axes.add_collection(... autolim=True) +* :ghpull:`30344`: Support fractional HiDPI in GTK4 backend +* :ghpull:`30326`: Backport PR #30321 on branch v3.10.x (Fix type annotation for Axes.get_legend() to include None) +* :ghpull:`30321`: Fix type annotation for Axes.get_legend() to include None +* :ghpull:`30287`: Backport PR #30286 on branch v3.10.x (Fix whitespace in _axes.py error message) +* :ghpull:`30288`: Backport PR #30283 on branch v3.10.x (changed the FAQ link to point to the correct path) +* :ghpull:`30293`: Backport PR #30289 on branch v3.10.x (DOC: Fix build with pybind11 3) +* :ghpull:`30283`: changed the FAQ link to point to the correct path +* :ghpull:`30286`: Fix whitespace in _axes.py error message +* :ghpull:`30271`: Reduce pause time in interactive timer test +* :ghpull:`30269`: Backport PR #30186 on branch v3.10.x (Fix figure legend when drawing stackplots) +* :ghpull:`30186`: Fix figure legend when drawing stackplots +* :ghpull:`30268`: Backport PR #30233 on branch v3.10.x (Check that stem input is 1D) +* :ghpull:`30233`: Check that stem input is 1D +* :ghpull:`30259`: Backport PR #30256 on branch v3.10.x (Time out in _get_executable_info) +* :ghpull:`30256`: Time out in _get_executable_info +* :ghpull:`30237`: Add explicit ``**options: Any`` for ``add_subplot`` method +* :ghpull:`30253`: Backport PR #30243 on branch v3.10.x (Fix FancyArrow rendering for zero-length arrows) +* :ghpull:`30243`: Fix FancyArrow rendering for zero-length arrows +* :ghpull:`30250`: Backport PR #30244 on branch v3.10.x (DOC: Recommend to use bare Figure instances for saving to file) +* :ghpull:`30247`: Backport PR #30246 on branch v3.10.x (chore: remove redundant words in comment) +* :ghpull:`30246`: chore: remove redundant words in comment +* :ghpull:`30240`: Backport PR #30236 on branch v3.10.x (Copy-edit the docstring of AuxTransformBox.) +* :ghpull:`30236`: Copy-edit the docstring of AuxTransformBox. +* :ghpull:`30234`: Backport PR #30209 on branch v3.10.x (Clean up Qt socket notifier to avoid spurious interrupt handler calls) +* :ghpull:`30209`: Clean up Qt socket notifier to avoid spurious interrupt handler calls +* :ghpull:`30195`: ci: Enable wheel builds on Python 3.14 +* :ghpull:`30229`: Backport PR #30221 on branch v3.10.x (BUG: fix future incompatibility with Pillow 13) +* :ghpull:`30221`: BUG: fix future incompatibility with Pillow 13 +* :ghpull:`30228`: Backport PR #30098 on branch v3.10.x (Fix label_outer in the presence of colorbars.) +* :ghpull:`30227`: Backport PR #30223 on branch v3.10.x (Polar log scale: fix inner patch boundary and spine location) +* :ghpull:`30098`: Fix label_outer in the presence of colorbars. +* :ghpull:`30223`: Polar log scale: fix inner patch boundary and spine location +* :ghpull:`30217`: Backport PR #30198 on branch v3.10.x (Implement Path.__deepcopy__ avoiding infinite recursion) +* :ghpull:`30198`: Implement Path.__deepcopy__ avoiding infinite recursion +* :ghpull:`30213`: Backport PR #30212 on branch v3.10.x ([Doc]: fix bug in release notes for matplotlib v3.5.0 and v3.7.0) +* :ghpull:`30189`: Backport PR #30180 on branch v3.10.x (DOC: expand polar example) +* :ghpull:`30167`: Backport PR #30162 on branch v3.10.x (TST: Fix runtime error checking NaN input to format_cursor_data) +* :ghpull:`30162`: TST: Fix runtime error checking NaN input to format_cursor_data +* :ghpull:`30146`: Backport PR #30144 on branch v3.10.x (js: Fix externally-controlled format strings) +* :ghpull:`30144`: js: Fix externally-controlled format strings +* :ghpull:`30140`: Backport PR #30118 on branch v3.10.x (CI: Skip jobs on forks) +* :ghpull:`30120`: Backport PR #30114 on branch v3.10.x (Fix _is_tensorflow_array.) +* :ghpull:`30122`: Backport PR #30119 on branch v3.10.x (Add some types to _mathtext.py) +* :ghpull:`30119`: Add some types to _mathtext.py +* :ghpull:`30114`: Fix _is_tensorflow_array. +* :ghpull:`30106`: Backport PR #30089 on branch v3.10.x (FIX: fix submerged margins algorithm being applied twice) +* :ghpull:`30089`: FIX: fix submerged margins algorithm being applied twice +* :ghpull:`30101`: Backport PR #30096 on branch v3.10.x (Fix OffsetBox custom picker) +* :ghpull:`30096`: Fix OffsetBox custom picker +* :ghpull:`30081`: Backport PR #30079 on branch v3.10.x (FIX: cast legend handles to list) +* :ghpull:`30079`: FIX: cast legend handles to list +* :ghpull:`30057`: Backport PR #29895 on branch v3.10.x (The 'lines.markeredgecolor' now doesn't interfere on the color of errorbar caps)" +* :ghpull:`29895`: The 'lines.markeredgecolor' now doesn't interfere on the color of errorbar caps +* :ghpull:`30033`: Backport PR #30029 on branch v3.10.x (Update diagram in subplots_adjust documentation to clarify parameters) + +Issues (18): + +* :ghissue:`30370`: [Bug]: matplotlib simple example fails in Python 3.14 +* :ghissue:`30218`: [Bug]: Rendering on Wayland with fractional scaling looks bad +* :ghissue:`30318`: [Bug]: type annotation of ``Axes.get_legend()`` misses ``None`` +* :ghissue:`30169`: [Doc]: Incorrect FAQ Link on Tutorials Page +* :ghissue:`30285`: [Bug]: Missing whitespace in _axes.py error message +* :ghissue:`30280`: [Bug]: Pillow 11.3 raises a deprecation warning when using TkAgg +* :ghissue:`30158`: [Bug]: Stackplot in SubFigure raises when drawing Legend +* :ghissue:`30216`: [Bug]: stem complaining about PyTorch's Tensor +* :ghissue:`30242`: [Bug]: Cannot create empty FancyArrow (expired numpy deprecation) +* :ghissue:`30249`: [Bug]: DeprecationWarning from Pillow 11.3.0 about 'mode' parameter of PIL.Image.fromarray() +* :ghissue:`29688`: [Bug]: "Bad file descriptor" raised repeatedly when plt.pause() interrupted in IPython +* :ghissue:`27305`: [Bug]: Axes.label_outer() does not work when there is a colorbar +* :ghissue:`30179`: [Bug]: Inner border is not rendered correctly when using log-scale and polar projection. +* :ghissue:`29157`: FUTURE BUG: reconsider how we deep-copy path objects +* :ghissue:`30152`: [Bug]: Test pipeline failure on windows +* :ghissue:`30076`: [Bug]: Layout Managers are confused by complex arrangement of sub-figures and gridspec's +* :ghissue:`30078`: [Bug]: legend no longer works with itertools.chain +* :ghissue:`29780`: [Bug]: Setting 'lines.markeredgecolor' affects color of errorbar caps. From b2358e7a537205b29ab0810b4bd94f036b3086dc Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 29 Aug 2025 18:13:43 -0500 Subject: [PATCH 063/110] Release prep v3.10.6 --- doc/_static/switcher.json | 4 ++-- doc/users/release_notes.rst | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/_static/switcher.json b/doc/_static/switcher.json index 7f4913137693..2d02ec01ae56 100644 --- a/doc/_static/switcher.json +++ b/doc/_static/switcher.json @@ -1,7 +1,7 @@ [ { "name": "3.10 (stable)", - "version": "3.10.5", + "version": "3.10.6", "url": "https://matplotlib.org/stable/", "preferred": true }, @@ -12,7 +12,7 @@ }, { "name": "3.9", - "version": "3.9.3", + "version": "3.9.4", "url": "https://matplotlib.org/3.9.4/" }, { diff --git a/doc/users/release_notes.rst b/doc/users/release_notes.rst index 4888e35948c7..1ca483ab15a2 100644 --- a/doc/users/release_notes.rst +++ b/doc/users/release_notes.rst @@ -21,6 +21,7 @@ Version 3.10 ../api/prev_api_changes/api_changes_3.10.1.rst ../api/prev_api_changes/api_changes_3.10.0.rst github_stats.rst + prev_whats_new/github_stats_3.10.5.rst prev_whats_new/github_stats_3.10.3.rst prev_whats_new/github_stats_3.10.1.rst prev_whats_new/github_stats_3.10.0.rst From 5cd38c3edcdf0792d0e6aded280a9b7a7de6146f Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 29 Aug 2025 18:21:02 -0500 Subject: [PATCH 064/110] REL: v3.10.6 This is a bugfix release in the 3.10.x series. Highlights from this release include: - Fix regression of hi-dpi support for Qt - Fix race condition in TexManager.make_dvi & make_png - Various documentation and other bugfixes From 1d52624ff79f0bd9c57fedd1ed334dad47ee4a03 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 29 Aug 2025 18:26:35 -0500 Subject: [PATCH 065/110] REL: bump from v3.10.6 From 22c649b6a4362f09a1080e0d46519639c022a590 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Fri, 29 Aug 2025 18:35:39 -0500 Subject: [PATCH 066/110] Zenodo v3.10.6 --- doc/_static/zenodo_cache/16999430.svg | 35 +++++++++++++++++++++++++++ doc/project/citing.rst | 3 +++ tools/cache_zenodo_svg.py | 1 + 3 files changed, 39 insertions(+) create mode 100644 doc/_static/zenodo_cache/16999430.svg diff --git a/doc/_static/zenodo_cache/16999430.svg b/doc/_static/zenodo_cache/16999430.svg new file mode 100644 index 000000000000..44c448643e91 --- /dev/null +++ b/doc/_static/zenodo_cache/16999430.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.16999430 + + + 10.5281/zenodo.16999430 + + + \ No newline at end of file diff --git a/doc/project/citing.rst b/doc/project/citing.rst index 249f568625db..ae2061e7349c 100644 --- a/doc/project/citing.rst +++ b/doc/project/citing.rst @@ -32,6 +32,9 @@ By version .. START OF AUTOGENERATED +v3.10.6 + .. image:: ../_static/zenodo_cache/16999430.svg + :target: https://doi.org/10.5281/zenodo.16999430 v3.10.5 .. image:: ../_static/zenodo_cache/16644850.svg :target: https://doi.org/10.5281/zenodo.16644850 diff --git a/tools/cache_zenodo_svg.py b/tools/cache_zenodo_svg.py index 59d6fce55162..c6783dd9f19d 100644 --- a/tools/cache_zenodo_svg.py +++ b/tools/cache_zenodo_svg.py @@ -63,6 +63,7 @@ def _get_xdg_cache_dir(): if __name__ == "__main__": data = { + "v3.10.6": "16999430", "v3.10.5": "16644850", "v3.10.3": "15375714", "v3.10.1": "14940554", From 66bc1ddaf5172f9b782ef4406af3274be82d60cb Mon Sep 17 00:00:00 2001 From: "Christine P. Chai" Date: Fri, 29 Aug 2025 20:09:39 -0700 Subject: [PATCH 067/110] DOC: pytz link should be from PyPI --- doc/install/dependencies.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/install/dependencies.rst b/doc/install/dependencies.rst index 4b006d9016e2..1fb463ab18e9 100644 --- a/doc/install/dependencies.rst +++ b/doc/install/dependencies.rst @@ -377,7 +377,7 @@ them will be skipped by pytest. .. _pandas: https://pypi.org/project/pandas/ .. _pikepdf: https://pypi.org/project/pikepdf/ .. _psutil: https://pypi.org/project/psutil/ -.. _pytz: https://fonts.google.com/noto/use#faq +.. _pytz: https://pypi.org/project/pytz/ .. _pytest-cov: https://pytest-cov.readthedocs.io/en/latest/ .. _pytest-timeout: https://pypi.org/project/pytest-timeout/ .. _pytest-xdist: https://pypi.org/project/pytest-xdist/ From 5906a4f0fb92144538f07c54ad4be8a9091cc8ed Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 31 Aug 2025 09:40:42 +0200 Subject: [PATCH 068/110] Update what's new according to suggestions --- doc/release/next_whats_new/pyplot-register-figure.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/doc/release/next_whats_new/pyplot-register-figure.rst b/doc/release/next_whats_new/pyplot-register-figure.rst index 0c186fe0ecb9..6df2676e0ffa 100644 --- a/doc/release/next_whats_new/pyplot-register-figure.rst +++ b/doc/release/next_whats_new/pyplot-register-figure.rst @@ -48,8 +48,9 @@ Technical detail: Standalone figures use `.FigureCanvasBase` as canvas. This is replaced by a backend-dependent subclass when registering with pyplot, and is reset to `.FigureCanvasBase` when the figure is closed. `.Figure.savefig` uses the current canvas to save the figure (if possible). Since `.FigureCanvasBase` -can not render the figure, when using savefig it will fallback to`.FigureCanvasAgg` -which is Agg-based. Any Agg-based backend will create the same file output, however +can not render the figure, when saving the figure, it will fallback to a suitable +canvas subclass, e.g. `.FigureCanvasAgg` for raster outputs such as png. +Any Agg-based backend will create the same file output, however There may be slight differences for non-Agg backends; e.g. if you use "GTK4Cairo" as interactive backend, ``fig.savefig("file.png")`` may create a slightly different image depending on whether the figure is registered with pyplot or not. In From 545892a5c1c6b873a65d9fb87c6cf721382b083f Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 31 Aug 2025 09:45:02 +0200 Subject: [PATCH 069/110] Move all super().destroy() calls to the end --- lib/matplotlib/backends/backend_nbagg.py | 2 +- lib/matplotlib/backends/backend_qt.py | 2 +- lib/matplotlib/backends/backend_wx.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index 543454ab25fd..3ffec0910d79 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -137,12 +137,12 @@ def _create_comm(self): return comm def destroy(self): - super().destroy() self._send_event('close') # need to copy comms as callbacks will modify this list for comm in list(self.web_sockets): comm.on_close() self.clearup_closed() + super().destroy() def clearup_closed(self): """Clear up any closed Comms.""" diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 68d89e1990bb..77ecf4b861c0 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -664,7 +664,6 @@ def show(self): self.window.raise_() def destroy(self, *args): - super().destroy() # check for qApp first, as PySide deletes it in its atexit handler if QtWidgets.QApplication.instance() is None: return @@ -674,6 +673,7 @@ def destroy(self, *args): if self.toolbar: self.toolbar.destroy() self.window.close() + super().destroy() def get_window_title(self): return self.window.windowTitle() diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 5219042e7971..6645077defc9 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -1007,12 +1007,12 @@ def show(self): def destroy(self, *args): # docstring inherited _log.debug("%s - destroy()", type(self)) - super().destroy() frame = self.frame if frame: # Else, may have been already deleted, e.g. when closing. # As this can be called from non-GUI thread from plt.close use # wx.CallAfter to ensure thread safety. wx.CallAfter(frame.Close) + super().destroy() def full_screen_toggle(self): # docstring inherited From 63e03292f990b55973bb07722809cc6fb80084f9 Mon Sep 17 00:00:00 2001 From: MQY <3463526515@qq.com> Date: Sun, 31 Aug 2025 16:01:08 +0800 Subject: [PATCH 070/110] Fix Line3DCollection with autolim=True for lines of different lengths (#30423) * Fix Line3DCollection with autolim=True for lines of different lengths When using Line3DCollection with autolim=True, the code was trying to convert ragged arrays (lines with different numbers of points) directly to a numpy array, which would fail. This fix extracts coordinates separately to avoid the issue. Fixes #30418 * fix lint * fix lint * Add test for Line3DCollection with autolim=True and ragged arrays * move test * Update lib/mpl_toolkits/mplot3d/tests/test_axes3d.py Co-authored-by: Elliott Sales de Andrade * Update lib/mpl_toolkits/mplot3d/tests/test_axes3d.py Co-authored-by: Elliott Sales de Andrade --------- Co-authored-by: Elliott Sales de Andrade --- lib/mpl_toolkits/mplot3d/axes3d.py | 6 +++-- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 22 +++++++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index c56e4c6b7039..3122b64bde73 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -2890,8 +2890,10 @@ def add_collection3d(self, col, zs=0, zdir='z', autolim=True, *, if autolim: if isinstance(col, art3d.Line3DCollection): - self.auto_scale_xyz(*np.array(col._segments3d).transpose(), - had_data=had_data) + # Handle ragged arrays by extracting coordinates separately + all_points = np.concatenate(col._segments3d) + self.auto_scale_xyz(all_points[:, 0], all_points[:, 1], + all_points[:, 2], had_data=had_data) elif isinstance(col, art3d.Poly3DCollection): self.auto_scale_xyz(col._faces[..., 0], col._faces[..., 1], diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index e6d11f793b46..370fd506530e 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -2689,3 +2689,25 @@ def test_ndarray_color_kwargs_value_error(): ax = fig.add_subplot(111, projection='3d') ax.scatter(1, 0, 0, color=np.array([0, 0, 0, 1])) fig.canvas.draw() + + +def test_line3dcollection_autolim_ragged(): + """Test Line3DCollection with autolim=True and lines of different lengths.""" + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + + # Create lines with different numbers of points (ragged arrays) + edges = [ + [(0, 0, 0), (1, 1, 1), (2, 2, 2)], # 3 points + [(0, 1, 0), (1, 2, 1)], # 2 points + [(1, 0, 1), (2, 1, 2), (3, 2, 3), (4, 3, 4)] # 4 points + ] + + # This should not raise an exception. + collections = ax.add_collection3d(art3d.Line3DCollection(edges), autolim=True) + + # Check that limits were computed correctly with margins + # The limits should include all points with default margins + assert np.allclose(ax.get_xlim3d(), (-0.08333333333333333, 4.083333333333333)) + assert np.allclose(ax.get_ylim3d(), (-0.0625, 3.0625)) + assert np.allclose(ax.get_zlim3d(), (-0.08333333333333333, 4.083333333333333)) From d65c5621757833a62574cc65e9acbb975ccf1909 Mon Sep 17 00:00:00 2001 From: Jonathan Reimer <41432658+jonathimer@users.noreply.github.com> Date: Tue, 2 Sep 2025 15:58:05 +0200 Subject: [PATCH 071/110] Add Linux Foundation Health Score badge to README Congrats! We have onboarded matplotlib to LFX Insights, the Linux Foundation's platform for monitoring the world's most critical open-source projects. https://insights.linuxfoundation.org/project/matplotlib --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 7b9c99597c0d..e7dce2a5f472 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Conda](https://img.shields.io/conda/vn/conda-forge/matplotlib)](https://anaconda.org/conda-forge/matplotlib) [![Downloads](https://img.shields.io/pypi/dm/matplotlib)](https://pypi.org/project/matplotlib) [![NUMFocus](https://img.shields.io/badge/powered%20by-NumFOCUS-orange.svg?style=flat&colorA=E1523D&colorB=007D8A)](https://numfocus.org) +[![LFX Health Score](https://insights.linuxfoundation.org/api/badge/health-score?project=matplotlib)](https://insights.linuxfoundation.org/project/matplotlib) [![Discourse help forum](https://img.shields.io/badge/help_forum-discourse-blue.svg)](https://discourse.matplotlib.org) [![Gitter](https://badges.gitter.im/matplotlib/matplotlib.svg)](https://gitter.im/matplotlib/matplotlib) From 0056a47621dd17870bfd84e52a475899c50df63b Mon Sep 17 00:00:00 2001 From: Aasma Gupta Date: Tue, 2 Sep 2025 22:13:29 +0530 Subject: [PATCH 072/110] Fix SVG rendering error in def update_background (#30490) Check if saving before updating blitting background --------- Co-authored-by: AASMA GUPTA Co-authored-by: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> --- lib/matplotlib/widgets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 034a9b4db7a0..f4adcd60f6cb 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2172,6 +2172,8 @@ def update_background(self, event): # `release` can call a draw event even when `ignore` is True. if not self.useblit: return + if self.canvas.is_saving(): + return # saving does not use blitting # Make sure that widget artists don't get accidentally included in the # background, by re-rendering the background if needed (and then # re-re-rendering the canvas with the visible widget artists). From 83b8e42bf848d9e532dc8173d52bf96514724cec Mon Sep 17 00:00:00 2001 From: Aaratrika-Shelly Date: Fri, 8 Aug 2025 19:18:31 +0000 Subject: [PATCH 073/110] MNT/DOC: Deprecate anchor in Axes3D.set_aspect --- .../deprecations/30364-AS.rst | 4 ++ lib/mpl_toolkits/mplot3d/axes3d.py | 38 ++++++++----------- lib/mpl_toolkits/mplot3d/tests/test_axes3d.py | 29 ++++++++++++++ 3 files changed, 49 insertions(+), 22 deletions(-) create mode 100644 doc/api/next_api_changes/deprecations/30364-AS.rst diff --git a/doc/api/next_api_changes/deprecations/30364-AS.rst b/doc/api/next_api_changes/deprecations/30364-AS.rst new file mode 100644 index 000000000000..4f5493b8b706 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30364-AS.rst @@ -0,0 +1,4 @@ +Parameters ``Axes3D.set_aspect(..., anchor=..., share=...)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The parameters *anchor* and *share* of `.Axes3D.set_aspect` are deprecated. +They had no effect on 3D axes and will be removed in a future version. diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 3122b64bde73..32da8dfde7aa 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -244,6 +244,8 @@ def _transformed_cube(self, vals): (minx, maxy, maxz)] return proj3d._proj_points(xyzs, self.M) + @_api.delete_parameter("3.11", "share") + @_api.delete_parameter("3.11", "anchor") def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): """ Set the aspect ratios. @@ -263,39 +265,31 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): 'equalyz' adapt the y and z axes to have equal aspect ratios. ========= ================================================== - adjustable : None or {'box', 'datalim'}, optional - If not *None*, this defines which parameter will be adjusted to - meet the required aspect. See `.set_adjustable` for further - details. + adjustable : {'box', 'datalim'}, default: 'box' + Defines which parameter to adjust to meet the aspect ratio. + + - 'box': Change the physical dimensions of the axes bounding box. + - 'datalim': Change the x, y, or z data limits. anchor : None or str or 2-tuple of float, optional - If not *None*, this defines where the Axes will be drawn if there - is extra space due to aspect constraints. The most common way to - specify the anchor are abbreviations of cardinal directions: - - ===== ===================== - value description - ===== ===================== - 'C' centered - 'SW' lower left corner - 'S' middle of bottom edge - 'SE' lower right corner - etc. - ===== ===================== - - See `~.Axes.set_anchor` for further details. + .. deprecated:: 3.11 + This parameter has no effect. share : bool, default: False - If ``True``, apply the settings to all shared Axes. + .. deprecated:: 3.11 + This parameter has no effect. See Also -------- mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect """ + if adjustable is None: + adjustable = 'box' + _api.check_in_list(['box', 'datalim'], adjustable=adjustable) _api.check_in_list(('auto', 'equal', 'equalxy', 'equalyz', 'equalxz'), aspect=aspect) - super().set_aspect( - aspect='auto', adjustable=adjustable, anchor=anchor, share=share) + + self.set_adjustable(adjustable) self._aspect = aspect if aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'): diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 370fd506530e..e38df4f80ba4 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -17,6 +17,7 @@ from matplotlib.patches import Circle, PathPatch from matplotlib.path import Path from matplotlib.text import Text +from matplotlib import _api import matplotlib.pyplot as plt import numpy as np @@ -2711,3 +2712,31 @@ def test_line3dcollection_autolim_ragged(): assert np.allclose(ax.get_xlim3d(), (-0.08333333333333333, 4.083333333333333)) assert np.allclose(ax.get_ylim3d(), (-0.0625, 3.0625)) assert np.allclose(ax.get_zlim3d(), (-0.08333333333333333, 4.083333333333333)) + + +def test_axes3d_set_aspect_deperecated_params(): + """ + Test that using the deprecated 'anchor' and 'share' kwargs in + set_aspect raises the correct warning. + """ + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + + # Test that providing the `anchor` parameter raises a deprecation warning. + with pytest.warns(_api.MatplotlibDeprecationWarning, match="'anchor' parameter"): + ax.set_aspect('equal', anchor='C') + + # Test that using the 'share' parameter is now deprecated. + with pytest.warns(_api.MatplotlibDeprecationWarning, match="'share' parameter"): + ax.set_aspect('equal', share=True) + + # Test that the `adjustable` parameter is correctly processed to satisfy + # code coverage. + ax.set_aspect('equal', adjustable='box') + assert ax.get_adjustable() == 'box' + + ax.set_aspect('equal', adjustable='datalim') + assert ax.get_adjustable() == 'datalim' + + with pytest.raises(ValueError, match="adjustable"): + ax.set_aspect('equal', adjustable='invalid_value') From 4383c87ac5006d3d5ab6f6b99e30334cb0ba93e5 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Tue, 2 Sep 2025 16:00:33 -0500 Subject: [PATCH 074/110] Revert 3.9 link --- doc/_static/switcher.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/_static/switcher.json b/doc/_static/switcher.json index 2d02ec01ae56..c71f2c8c4859 100644 --- a/doc/_static/switcher.json +++ b/doc/_static/switcher.json @@ -12,8 +12,8 @@ }, { "name": "3.9", - "version": "3.9.4", - "url": "https://matplotlib.org/3.9.4/" + "version": "3.9.3", + "url": "https://matplotlib.org/3.9.3/" }, { "name": "3.8", From a6356715f9952078f73e490bd2987417c3c2b970 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Thu, 17 Apr 2025 13:09:43 +0200 Subject: [PATCH 075/110] Parse {lua,xe}tex-generated dvi in dviread. --- lib/matplotlib/backends/backend_pdf.py | 7 +- lib/matplotlib/cbook.py | 10 +- lib/matplotlib/dviread.py | 308 +++++++++++++----- lib/matplotlib/dviread.pyi | 20 +- lib/matplotlib/testing/__init__.py | 4 +- lib/matplotlib/testing/__init__.pyi | 3 + .../baseline_images/dviread/lualatex.json | 1 + .../baseline_images/dviread/pdflatex.json | 1 + .../tests/baseline_images/dviread/test.dvi | Bin 856 -> 0 bytes .../tests/baseline_images/dviread/test.json | 94 ------ .../tests/baseline_images/dviread/test.tex | 16 +- .../baseline_images/dviread/xelatex.json | 1 + lib/matplotlib/tests/test_dviread.py | 56 +++- lib/matplotlib/textpath.py | 4 +- 14 files changed, 313 insertions(+), 212 deletions(-) create mode 100644 lib/matplotlib/tests/baseline_images/dviread/lualatex.json create mode 100644 lib/matplotlib/tests/baseline_images/dviread/pdflatex.json delete mode 100644 lib/matplotlib/tests/baseline_images/dviread/test.dvi delete mode 100644 lib/matplotlib/tests/baseline_images/dviread/test.json create mode 100644 lib/matplotlib/tests/baseline_images/dviread/xelatex.json diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index a75a8a86eb92..d63808eb3925 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -1035,11 +1035,10 @@ def _embedTeXFont(self, dvifont): fontdict['Encoding'] = self._generate_encoding(encoding) fc = fontdict['FirstChar'] = min(encoding.keys(), default=0) lc = fontdict['LastChar'] = max(encoding.keys(), default=255) - # Convert glyph widths from TeX 12.20 fixed point to 1/1000 text space units - tfm = dvifont._tfm - widths = [(1000 * metrics.tex_width) >> 20 - if (metrics := tfm.get_metrics(char)) else 0 + font_metrics = dvifont._metrics + widths = [(1000 * glyph_metrics.tex_width) >> 20 + if (glyph_metrics := font_metrics.get_metrics(char)) else 0 for char in range(fc, lc + 1)] fontdict['Widths'] = widthsObject = self.reserveObject('glyph widths') self.writeObject(widthsObject, widths) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index a09780965b0c..8b438e7d79b5 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -43,16 +43,20 @@ class _ExceptionInfo: users and result in incorrect tracebacks. """ - def __init__(self, cls, *args): + def __init__(self, cls, *args, notes=None): self._cls = cls self._args = args + self._notes = notes if notes is not None else [] @classmethod def from_exception(cls, exc): - return cls(type(exc), *exc.args) + return cls(type(exc), *exc.args, notes=getattr(exc, "__notes__", [])) def to_exception(self): - return self._cls(*self._args) + exc = self._cls(*self._args) + for note in self._notes: + exc.add_note(note) + return exc def _get_running_interactive_framework(): diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 9e8b6a5facf5..1a0f9219a498 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -26,12 +26,14 @@ import subprocess import sys from collections import namedtuple -from functools import cache, lru_cache, partial, wraps +from functools import cache, cached_property, lru_cache, partial, wraps from pathlib import Path +import fontTools.agl import numpy as np from matplotlib import _api, cbook, font_manager +from matplotlib.ft2font import LoadFlags _log = logging.getLogger(__name__) @@ -67,42 +69,15 @@ class Text(namedtuple('Text', 'x y font glyph width')): """ A glyph in the dvi file. - The *x* and *y* attributes directly position the glyph. The *font*, - *glyph*, and *width* attributes are kept public for back-compatibility, - but users wanting to draw the glyph themselves are encouraged to instead - load the font specified by `font_path` at `font_size`, warp it with the - effects specified by `font_effects`, and load the glyph at the FreeType - glyph `index`. - """ - - def _get_pdftexmap_entry(self): - return PsfontsMap(find_tex_file("pdftex.map"))[self.font.texname] - - @property - def font_path(self): - """The `~pathlib.Path` to the font for this glyph.""" - psfont = self._get_pdftexmap_entry() - if psfont.filename is None: - raise ValueError("No usable font file found for {} ({}); " - "the font may lack a Type-1 version" - .format(psfont.psname.decode("ascii"), - psfont.texname.decode("ascii"))) - return Path(psfont.filename) + In order to render the glyph, load the glyph at index ``text.index`` + from the font at ``text.font.resolve_path()`` with size ``text.font.size``, + warped with ``text.font.effects``, then draw it at position + ``(text.x, text.y)``. - @property - def font_size(self): - """The font size.""" - return self.font.size - - @property - def font_effects(self): - """ - The "font effects" dict for this glyph. - - This dict contains the values for this glyph of SlantFont and - ExtendFont (if any), read off :file:`pdftex.map`. - """ - return self._get_pdftexmap_entry().effects + ``text.glyph`` is the glyph number actually stored in the dvi file (whose + interpretation depends on the font). ``text.width`` is the glyph width in + dvi units. + """ @property def index(self): @@ -112,25 +87,51 @@ def index(self): # See DviFont._index_dvi_to_freetype for details on the index mapping. return self.font._index_dvi_to_freetype(self.glyph) - @property # To be deprecated together with font_size, font_effects. + font_path = property(lambda self: self.font.resolve_path()) + font_size = property(lambda self: self.font.size) + font_effects = property(lambda self: self.font.effects) + + @property # To be deprecated together with font_path, font_size, font_effects. def glyph_name_or_index(self): """ - Either the glyph name or the native charmap glyph index. - - If :file:`pdftex.map` specifies an encoding for this glyph's font, that - is a mapping of glyph indices to Adobe glyph names; use it to convert - dvi indices to glyph names. Callers can then convert glyph names to - glyph indices (with FT_Get_Name_Index/get_name_index), and load the - glyph using FT_Load_Glyph/load_glyph. - - If :file:`pdftex.map` specifies no encoding, the indices directly map - to the font's "native" charmap; glyphs should directly load using - FT_Load_Char/load_char after selecting the native charmap. + The glyph name, the native charmap glyph index, or the raw glyph index. + + If the font is a TrueType file (which can currently only happen for + DVI files generated by xetex or luatex), then this number is the raw + index of the glyph, which can be passed to FT_Load_Glyph/load_glyph. + + Otherwise, the font is a PostScript font. For such fonts, if + :file:`pdftex.map` specifies an encoding for this glyph's font, + that is a mapping of glyph indices to Adobe glyph names; which + is used by this property to convert dvi numbers to glyph names. + Callers can then convert glyph names to glyph indices (with + FT_Get_Name_Index/get_name_index), and load the glyph using + FT_Load_Glyph/load_glyph. + + If :file:`pdftex.map` specifies no encoding for a PostScript font, + this number is an index to the font's "native" charmap; glyphs should + directly load using FT_Load_Char/load_char after selecting the native + charmap. """ + # The last section is only true on luatex since luaotfload 3.23; this + # must be checked by the code generated by texmanager. (luaotfload's + # docs states "No one should rely on the mapping between DVI character + # codes and font glyphs [prior to v3.15] unless they tightly + # control all involved versions and are deeply familiar with the + # implementation", but a further mapping bug was fixed in luaotfload + # commit 8f2dca4, first included in v3.23). entry = self._get_pdftexmap_entry() return (_parse_enc(entry.encoding)[self.glyph] if entry.encoding is not None else self.glyph) + def _as_unicode_or_name(self): + if self.font.subfont: + raise NotImplementedError("Indexing TTC fonts is not supported yet") + face = font_manager.get_font(self.font.resolve_path()) + glyph_name = face.get_glyph_name(self.index) + glyph_str = fontTools.agl.toUnicode(glyph_name) + return glyph_str or glyph_name + # Opcode argument parsing # @@ -408,7 +409,7 @@ def _put_char_real(self, char): scale = font._scale for x, y, f, g, w in font._vf[char].text: newf = DviFont(scale=_mul1220(scale, f._scale), - tfm=f._tfm, texname=f.texname, vf=f._vf) + metrics=f._metrics, texname=f.texname, vf=f._vf) self.text.append(Text(self.h + _mul1220(x, scale), self.v + _mul1220(y, scale), newf, g, newf._width_of(g))) @@ -504,10 +505,21 @@ def _fnt_def(self, k, c, s, d, a, l): def _fnt_def_real(self, k, c, s, d, a, l): n = self.file.read(a + l) - fontname = n[-l:].decode('ascii') + fontname = n[-l:] + if fontname.startswith(b"[") and c == 0x4c756146: # c == "LuaF" + # See https://chat.stackexchange.com/rooms/106428 (and also + # https://tug.org/pipermail/dvipdfmx/2021-January/000168.html). + # AFAICT luatex's dvi drops info re: OpenType variation-axis values. + self.fonts[k] = DviFont.from_luatex(s, n) + return + fontname = fontname.decode("ascii") try: tfm = _tfmfile(fontname) except FileNotFoundError as exc: + if fontname.startswith("[") and fontname.endswith(";") and c == 0: + exc.add_note( + "This dvi file was likely generated with a too-old " + "version of luaotfload; luaotfload 3.23 is required.") # Explicitly allow defining missing fonts for Vf support; we only # register an error when trying to load a glyph from a missing font # and throw that error in Dvi._read. For Vf, _finalize_packet @@ -521,12 +533,12 @@ def _fnt_def_real(self, k, c, s, d, a, l): vf = _vffile(fontname) except FileNotFoundError: vf = None - self.fonts[k] = DviFont(scale=s, tfm=tfm, texname=n, vf=vf) + self.fonts[k] = DviFont(scale=s, metrics=tfm, texname=n, vf=vf) @_dispatch(247, state=_dvistate.pre, args=('u1', 'u4', 'u4', 'u4', 'u1')) def _pre(self, i, num, den, mag, k): self.file.read(k) # comment in the dvi file - if i != 2: + if i not in [2, 7]: # 2: pdftex, luatex; 7: xetex raise ValueError(f"Unknown dvi format {i}") if num != 25400000 or den != 7227 * 2**16: raise ValueError("Nonstandard units in dvi file") @@ -547,13 +559,66 @@ def _post(self, _): # TODO: actually read the postamble and finale? # currently post_post just triggers closing the file - @_dispatch(249) - def _post_post(self, _): + @_dispatch(249, args=()) + def _post_post(self): + raise NotImplementedError + + @_dispatch(250, args=()) + def _begin_reflect(self): + raise NotImplementedError + + @_dispatch(251, args=()) + def _end_reflect(self): raise NotImplementedError - @_dispatch(min=250, max=255) - def _malformed(self, offset): - raise ValueError(f"unknown command: byte {250 + offset}") + @_dispatch(252, args=()) + def _define_native_font(self): + k = self._read_arg(4, signed=False) + s = self._read_arg(4, signed=False) + flags = self._read_arg(2, signed=False) + l = self._read_arg(1, signed=False) + n = self.file.read(l) + i = self._read_arg(4, signed=False) + effects = {} + if flags & 0x0200: + effects["rgba"] = [self._read_arg(1, signed=False) for _ in range(4)] + if flags & 0x1000: + effects["extend"] = self._read_arg(4, signed=True) / 65536 + if flags & 0x2000: + effects["slant"] = self._read_arg(4, signed=True) / 65536 + if flags & 0x4000: + effects["embolden"] = self._read_arg(4, signed=True) / 65536 + self.fonts[k] = DviFont.from_xetex(s, n, i, effects) + + @_dispatch(253, args=()) + def _set_glyphs(self): + w = self._read_arg(4, signed=False) + k = self._read_arg(2, signed=False) + xy = [self._read_arg(4, signed=True) for _ in range(2 * k)] + g = [self._read_arg(2, signed=False) for _ in range(k)] + font = self.fonts[self.f] + for i in range(k): + self.text.append(Text(self.h + xy[2 * i], self.v + xy[2 * i + 1], + font, g[i], font._width_of(g[i]))) + self.h += w + + @_dispatch(254, args=()) + def _set_text_and_glyphs(self): + l = self._read_arg(2, signed=False) + t = self.file.read(2 * l) # utf16 + w = self._read_arg(4, signed=False) + k = self._read_arg(2, signed=False) + xy = [self._read_arg(4, signed=True) for _ in range(2 * k)] + g = [self._read_arg(2, signed=False) for _ in range(k)] + font = self.fonts[self.f] + for i in range(k): + self.text.append(Text(self.h + xy[2 * i], self.v + xy[2 * i + 1], + font, g[i], font._width_of(g[i]))) + self.h += w + + @_dispatch(255) + def _malformed(self, raw): + raise ValueError("unknown command: byte 255") class DviFont: @@ -571,10 +636,10 @@ class DviFont: ---------- scale : float Factor by which the font is scaled from its natural size. - tfm : Tfm + metrics : Tfm | TtfMetrics TeX font metrics for this font texname : bytes - Name of the font as used internally by TeX and friends, as an ASCII + Name of the font as used internally in the DVI file, as an ASCII bytestring. This is usually very different from any external font names; `PsfontsMap` can be used to find the external name of the font. vf : Vf @@ -590,17 +655,54 @@ class DviFont: Size of the font in Adobe points, converted from the slightly smaller TeX points. """ - __slots__ = ('texname', 'size', '_scale', '_vf', '_tfm', '_encoding') - def __init__(self, scale, tfm, texname, vf): + def __init__(self, scale, metrics, texname, vf): _api.check_isinstance(bytes, texname=texname) self._scale = scale - self._tfm = tfm + self._metrics = metrics self.texname = texname self._vf = vf - self.size = scale * (72.0 / (72.27 * 2**16)) + self._path = None self._encoding = None + @classmethod + def from_luatex(cls, scale, texname): + path_b, sep, rest = texname[1:].rpartition(b"]") + if not (texname.startswith(b"[") and sep and rest[:1] in [b"", b":"]): + raise ValueError(f"Invalid modern font name: {texname}") + # utf8 on Windows, not utf16! + path = path_b.decode("utf8") if os.name == "nt" else os.fsdecode(path_b) + subfont = 0 + effects = {} + if rest[1:]: + for kv in rest[1:].decode("ascii").split(";"): + key, val = kv.split("=", 1) + if key == "index": + subfont = val + elif key in ["embolden", "slant", "extend"]: + effects[key] = int(val) / 65536 + else: + _log.warning("Ignoring invalid key-value pair: %r", kv) + metrics = TtfMetrics(path) + font = cls(scale, metrics, texname, vf=None) + font._path = Path(path) + font.subfont = subfont + font.effects = effects + return font + + @classmethod + def from_xetex(cls, scale, texname, subfont, effects): + # utf8 on Windows, not utf16! + path = texname.decode("utf8") if os.name == "nt" else os.fsdecode(texname) + metrics = TtfMetrics(path) + font = cls(scale, metrics, b"[" + texname + b"]", vf=None) + font._path = Path(path) + font.subfont = subfont + font.effects = effects + return font + + size = property(lambda self: self._scale * (72.0 / (72.27 * 2**16))) + widths = _api.deprecated("3.11")(property(lambda self: [ (1000 * self._tfm.width.get(char, 0)) >> 20 for char in range(max(self._tfm.width, default=-1) + 1)])) @@ -629,7 +731,7 @@ def __repr__(self): def _width_of(self, char): """Width of char in dvi units.""" - metrics = self._tfm.get_metrics(char) + metrics = self._metrics.get_metrics(char) if metrics is None: _log.debug('No width for char %d in font %s.', char, self.texname) return 0 @@ -637,7 +739,7 @@ def _width_of(self, char): def _height_depth_of(self, char): """Height and depth of char in dvi units.""" - metrics = self._tfm.get_metrics(char) + metrics = self._metrics.get_metrics(char) if metrics is None: _log.debug('No metrics for char %d in font %s', char, self.texname) return [0, 0] @@ -654,26 +756,42 @@ def _height_depth_of(self, char): hd[-1] = 0 return hd + def resolve_path(self): + if self._path is None: + psfont = PsfontsMap(find_tex_file("pdftex.map"))[self.texname] + if psfont.filename is None: + raise ValueError("No usable font file found for {} ({}); " + "the font may lack a Type-1 version" + .format(psfont.psname.decode("ascii"), + psfont.texname.decode("ascii"))) + self._path = Path(psfont.filename) + return self._path + + @cached_property + def subfont(self): + return 0 + + @cached_property + def effects(self): + return PsfontsMap(find_tex_file("pdftex.map"))[self.texname].effects + def _index_dvi_to_freetype(self, idx): """Convert dvi glyph indices to FreeType ones.""" # Glyphs indices stored in the dvi file map to FreeType glyph indices # (i.e., which can be passed to FT_Load_Glyph) in various ways: + # - for xetex & luatex "native fonts", dvi indices are directly equal + # to FreeType indices. # - if pdftex.map specifies an ".enc" file for the font, that file maps # dvi indices to Adobe glyph names, which can then be converted to # FreeType glyph indices with FT_Get_Name_Index. # - if no ".enc" file is specified, then the font must be a Type 1 # font, and dvi indices directly index into the font's CharStrings # vector. - # - (xetex & luatex, currently unsupported, can also declare "native - # fonts", for which dvi indices are equal to FreeType indices.) + if self.texname.startswith(b"["): + return idx if self._encoding is None: + face = font_manager.get_font(self.resolve_path()) psfont = PsfontsMap(find_tex_file("pdftex.map"))[self.texname] - if psfont.filename is None: - raise ValueError("No usable font file found for {} ({}); " - "the font may lack a Type-1 version" - .format(psfont.psname.decode("ascii"), - psfont.texname.decode("ascii"))) - face = font_manager.get_font(psfont.filename) if psfont.encoding: self._encoding = [face.get_name_index(name) for name in _parse_enc(psfont.encoding)] @@ -882,6 +1000,27 @@ def get_metrics(self, idx): property(lambda self: {c: m.tex_depth for c, m in self._glyph_metrics})) +class TtfMetrics: + def __init__(self, filename): + self._face = font_manager.get_font(filename, hinting_factor=1) + + def get_metrics(self, idx): + # _mul1220 uses a truncating bitshift for compatibility with dvitype. + # When upem is 2048 the conversion to 12.20 is exact, but when + # upem is 1000 (e.g. lmroman10-regular.otf) the metrics themselves + # are not exactly representable as 12.20 fp. Manual testing via + # \sbox0{x}\count0=\wd0\typeout{\the\count0} suggests that metrics + # are rounded (not truncated) after conversion to 12.20 and before + # multiplication by the scale. + upem = self._face.units_per_EM # Usually 2048 or 1000. + g = self._face.load_glyph(idx, LoadFlags.NO_SCALE) + return TexMetrics( + tex_width=round(g.horiAdvance / upem * 2**20), + tex_height=round(g.horiBearingY / upem * 2**20), + tex_depth=round((g.height - g.horiBearingY) / upem * 2**20), + ) + + PsFont = namedtuple('PsFont', 'texname psname effects encoding filename') @@ -1179,10 +1318,6 @@ def _fontfile(cls, suffix, texname): import itertools from argparse import ArgumentParser - import fontTools.agl - - from matplotlib.ft2font import FT2Font - parser = ArgumentParser() parser.add_argument("filename") parser.add_argument("dpi", nargs="?", type=float, default=None) @@ -1197,17 +1332,18 @@ def _print_fields(*args): print(f"=== NEW PAGE === " f"(w: {page.width}, h: {page.height}, d: {page.descent})") print("--- GLYPHS ---") - for font, group in itertools.groupby( - page.text, lambda text: text.font): - psfont = fontmap[font.texname] - fontpath = psfont.filename - print(f"font: {font.texname.decode('latin-1')} " - f"(scale: {font._scale / 2 ** 20}) at {fontpath}") - face = FT2Font(fontpath) + for font, group in itertools.groupby(page.text, lambda text: text.font): + font_name = (font.texname.decode("utf8") if os.name == "nt" + else os.fsdecode(font.texname)) + if isinstance(font._metrics, Tfm): + print(f"font: {font_name} at {font.resolve_path()}") + else: + print(f"font: {font_name}") + print(f"scale: {font._scale / 2 ** 20}") _print_fields("x", "y", "glyph", "chr", "w") for text in group: - glyph_str = fontTools.agl.toUnicode(face.get_glyph_name(text.index)) - _print_fields(text.x, text.y, text.glyph, glyph_str, text.width) + _print_fields(text.x, text.y, text.glyph, + text._as_unicode_or_name(), text.width) if page.boxes: print("--- BOXES ---") _print_fields("x", "y", "h", "w") diff --git a/lib/matplotlib/dviread.pyi b/lib/matplotlib/dviread.pyi index 82c0238d39d1..1c24ff1c28a9 100644 --- a/lib/matplotlib/dviread.pyi +++ b/lib/matplotlib/dviread.pyi @@ -58,16 +58,28 @@ class Dvi: class DviFont: texname: bytes - size: float def __init__( - self, scale: float, tfm: Tfm, texname: bytes, vf: Vf | None + self, scale: float, metrics: Tfm | TtfMetrics, texname: bytes, vf: Vf | None ) -> None: ... + @classmethod + def from_luatex(cls, scale: float, texname: bytes) -> DviFont: ... + @classmethod + def from_xetex( + cls, scale: float, texname: bytes, subfont: int, effects: dict[str, float] + ) -> DviFont: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... @property + def size(self) -> float: ... + @property def widths(self) -> list[int]: ... @property def fname(self) -> str: ... + def resolve_path(self) -> Path: ... + @property + def subfont(self) -> int: ... + @property + def effects(self) -> dict[str, float]: ... class Vf(Dvi): def __init__(self, filename: str | os.PathLike) -> None: ... @@ -93,6 +105,10 @@ class Tfm: @property def depth(self) -> dict[int, int]: ... +class TtfMetrics: + def __init__(self, filename: str | os.PathLike) -> None: ... + def get_metrics(self, idx: int) -> TexMetrics: ... + class PsFont(NamedTuple): texname: bytes psname: bytes diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index 904ee5d73db4..eff079efe887 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -54,7 +54,7 @@ def setup(): def subprocess_run_for_testing(command, env=None, timeout=60, stdout=None, stderr=None, check=False, text=True, - capture_output=False): + capture_output=False, **kwargs): """ Create and run a subprocess. @@ -97,7 +97,7 @@ def subprocess_run_for_testing(command, env=None, timeout=60, stdout=None, command, env=env, timeout=timeout, check=check, stdout=stdout, stderr=stderr, - text=text + text=text, **kwargs ) except BlockingIOError: if sys.platform == "cygwin": diff --git a/lib/matplotlib/testing/__init__.pyi b/lib/matplotlib/testing/__init__.pyi index 7763cb6a9769..accf973615fa 100644 --- a/lib/matplotlib/testing/__init__.pyi +++ b/lib/matplotlib/testing/__init__.pyi @@ -16,6 +16,7 @@ def subprocess_run_for_testing( *, text: Literal[True], capture_output: bool = ..., + **kwargs, ) -> subprocess.CompletedProcess[str]: ... @overload def subprocess_run_for_testing( @@ -27,6 +28,7 @@ def subprocess_run_for_testing( check: bool = ..., text: Literal[False] = ..., capture_output: bool = ..., + **kwargs, ) -> subprocess.CompletedProcess[bytes]: ... @overload def subprocess_run_for_testing( @@ -38,6 +40,7 @@ def subprocess_run_for_testing( check: bool = ..., text: bool = ..., capture_output: bool = ..., + **kwargs, ) -> subprocess.CompletedProcess[bytes] | subprocess.CompletedProcess[str]: ... def subprocess_run_helper( func: Callable[[], None], diff --git a/lib/matplotlib/tests/baseline_images/dviread/lualatex.json b/lib/matplotlib/tests/baseline_images/dviread/lualatex.json new file mode 100644 index 000000000000..8f2d95017ec7 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/dviread/lualatex.json @@ -0,0 +1 @@ +[{"text": [[5046272, 4128768, "A", "lmroman10-regular.otf", 9.96, {}], [5756027, 4128768, "L", "lmroman10-regular.otf", 9.96, {}], [5929697, 4012179, "A", "lmroman7-regular.otf", 6.97, {}], [6218125, 4128768, "T", "lmroman10-regular.otf", 9.96, {}], [6582045, 4269998, "E", "lmroman10-regular.otf", 9.96, {}], [6946425, 4128768, "X", "lmroman10-regular.otf", 9.96, {}], [7656180, 4128768, "d", "DejaVuSans.ttf", 9.96, {"extend": 1.25, "slant": 0.25, "embolden": 0.25}], [8072180, 4128768, "o", "DejaVuSans.ttf", 9.96, {"extend": 1.25, "slant": 0.25, "embolden": 0.25}], [8473140, 4128768, "c", "DejaVuSans.ttf", 9.96, {"extend": 1.25, "slant": 0.25, "embolden": 0.25}], [8833460, 4128768, ".", "DejaVuSans.ttf", 9.96, {"extend": 1.25, "slant": 0.25, "embolden": 0.25}]], "boxes": []}, {"text": [[13686374, 5056284, "\u03c0", "cmmi5.pfb", 4.98, {}], [13716923, 5390308, "2", "cmr5.pfb", 4.98, {}], [13355110, 5463127, "integraldisplay", "cmex10.pfb", 9.96, {}], [13406537, 7324364, "0", "cmr7.pfb", 6.97, {}], [14010471, 5627696, "parenleftBig", "cmex10.pfb", 9.96, {}], [14937513, 5911796, "x", "cmmi10.pfb", 9.96, {}], [14480510, 6804696, "s", "lmroman10-regular.otf", 9.96, {}], [14738721, 6804696, "i", "lmroman10-regular.otf", 9.96, {}], [14920911, 6804696, "n", "lmroman10-regular.otf", 9.96, {}], [15394516, 6804696, "x", "cmmi10.pfb", 9.96, {}], [15847715, 5627696, "parenrightBig", "cmex10.pfb", 9.96, {}], [16239111, 5763501, "2", "cmr7.pfb", 6.97, {}], [16642338, 6355152, "d", "lmroman10-regular.otf", 9.96, {}], [17006718, 6355152, "x", "cmmi10.pfb", 9.96, {}]], "boxes": [[13686374, 5130818, 26213, 284106], [14480510, 6204418, 26213, 1288562]]}] diff --git a/lib/matplotlib/tests/baseline_images/dviread/pdflatex.json b/lib/matplotlib/tests/baseline_images/dviread/pdflatex.json new file mode 100644 index 000000000000..4754b722aa58 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/dviread/pdflatex.json @@ -0,0 +1 @@ +[{"text": [[5046272, 4128768, "A", "cmr10.pfb", 9.96, {}], [5756246, 4128768, "L", "cmr10.pfb", 9.96, {}], [5929917, 3994421, "A", "cmr7.pfb", 6.97, {}], [6218464, 4128768, "T", "cmr10.pfb", 9.96, {}], [6582530, 4269852, "E", "cmr10.pfb", 9.96, {}], [6946620, 4128768, "X", "cmr10.pfb", 9.96, {}], [7656594, 4128768, "d", "cmr10.pfb", 9.96, {}], [8020684, 4128768, "o", "cmr10.pfb", 9.96, {}], [8366570, 4128768, "c", "cmr10.pfb", 9.96, {}], [8657841, 4128768, ".", "cmr10.pfb", 9.96, {}]], "boxes": []}, {"text": [[13686591, 5056284, "\u03c0", "cmmi5.pfb", 4.98, {}], [13717140, 5390308, "2", "cmr5.pfb", 4.98, {}], [13355327, 5463127, "integraldisplay", "cmex10.pfb", 9.96, {}], [13406754, 7324364, "0", "cmr7.pfb", 6.97, {}], [14010688, 5627696, "parenleftBig", "cmex10.pfb", 9.96, {}], [14937658, 5911796, "x", "cmmi10.pfb", 9.96, {}], [14480727, 6804696, "s", "cmr10.pfb", 9.96, {}], [14739230, 6804696, "i", "cmr10.pfb", 9.96, {}], [14921275, 6804696, "n", "cmr10.pfb", 9.96, {}], [15394589, 6804696, "x", "cmmi10.pfb", 9.96, {}], [15847788, 5627696, "parenrightBig", "cmex10.pfb", 9.96, {}], [16239184, 5763501, "2", "cmr7.pfb", 6.97, {}], [16642411, 6355152, "d", "cmr10.pfb", 9.96, {}], [17006501, 6355152, "x", "cmmi10.pfb", 9.96, {}]], "boxes": [[13686591, 5130818, 26213, 284106], [14480727, 6204418, 26213, 1288418]]}] diff --git a/lib/matplotlib/tests/baseline_images/dviread/test.dvi b/lib/matplotlib/tests/baseline_images/dviread/test.dvi deleted file mode 100644 index 93751ffdcba0df980ecca01aec8ba561a4e4f485..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 856 zcmey)#MnIPfQ&T*5HP=xRtQOrP{=PWDJU&bFfuSS)iX5EGcd6-G%&U32C85LDI)~_ z1Hl5ON(P4B1%DSaFf3rQ2Q!}l$%(!U44>J(KPFT%Z~=`0Vb z8P~pv%#SM~CYPiZmrPE{pWrT=T$-DjH(|%)lD;K8Ae-$N7}D~KKvsa%Wagz$&P^;S z$jL9s$xKo&o}5yaS(KWX(zmb|th5E>RmMmLhQ7Y}ia##&_RfEI&jCpO4dCgW#Bkxd z{bvq;-diyYtRNpie36@Jx>>RhEIFT1S*;UjUn&Dj=&AY4Ul;XGC=dP2+QvLP3a$g> z6VnYweLxEY6%X_RO+9_me*UNA_RkEzHkJy(!-p+7H?_jhV09EwA>YYAKy_y(DQz_9 z>+1#Sxq8xJ{+DM7s|A4O00rj%v{3}xYpadsW>yQZzAu`f^^rJ!(~FM0CMdye3O)a1;>9L2uA9#B$Y0%Br-7M`TCDKRgE diff --git a/lib/matplotlib/tests/baseline_images/dviread/test.json b/lib/matplotlib/tests/baseline_images/dviread/test.json deleted file mode 100644 index 0809cb9531f1..000000000000 --- a/lib/matplotlib/tests/baseline_images/dviread/test.json +++ /dev/null @@ -1,94 +0,0 @@ -[ - { - "text": [ - [5046272, 4128768, "T", "cmr10", 9.96], - [5519588, 4128768, "h", "cmr10", 9.96], - [5883678, 4128768, "i", "cmr10", 9.96], - [6065723, 4128768, "s", "cmr10", 9.96], - [6542679, 4128768, "i", "cmr10", 9.96], - [6724724, 4128768, "s", "cmr10", 9.96], - [7201680, 4128768, "a", "cmr10", 9.96], - [7747814, 4128768, "L", "cmr10", 9.96], - [7921485, 3994421, "A", "cmr7", 6.97], - [8210032, 4128768, "T", "cmr10", 9.96], - [8574098, 4269852, "E", "cmr10", 9.96], - [8938188, 4128768, "X", "cmr10", 9.96], - [9648162, 4128768, "t", "cmr10", 9.96], - [9903025, 4128768, "e", "cmr10", 9.96], - [10194296, 4128768, "s", "cmr10", 9.96], - [10452799, 4128768, "t", "cmr10", 9.96], - [10926115, 4128768, "d", "cmr10", 9.96], - [11290205, 4128768, "o", "cmr10", 9.96], - [11636091, 4128768, "c", "cmr10", 9.96], - [11927362, 4128768, "u", "cmr10", 9.96], - [12291452, 4128768, "m", "cmr10", 9.96], - [12837587, 4128768, "e", "cmr10", 9.96], - [13128858, 4128768, "n", "cmr10", 9.96], - [13474743, 4128768, "t", "cmr10", 9.96], - [4063232, 4915200, "f", "cmr10", 9.96], - [4263482, 4915200, "o", "cmr10", 9.96], - [4591163, 4915200, "r", "cmr10", 9.96], - [5066299, 4915200, "t", "cmr10", 9.96], - [5321162, 4915200, "e", "cmr10", 9.96], - [5612433, 4915200, "s", "cmr10", 9.96], - [5870936, 4915200, "t", "cmr10", 9.96], - [6125799, 4915200, "i", "cmr10", 9.96], - [6307844, 4915200, "n", "cmr10", 9.96], - [6671934, 4915200, "g", "cmr10", 9.96], - [7218068, 4915200, "m", "cmr10", 9.96], - [7764203, 4915200, "a", "cmr10", 9.96], - [8091884, 4915200, "t", "cmr10", 9.96], - [8346747, 4915200, "p", "cmr10", 9.96], - [8710837, 4915200, "l", "cmr10", 9.96], - [8892882, 4915200, "o", "cmr10", 9.96], - [9220563, 4915200, "t", "cmr10", 9.96], - [9475426, 4915200, "l", "cmr10", 9.96], - [9657471, 4915200, "i", "cmr10", 9.96], - [9839516, 4915200, "b", "cmr10", 9.96], - [10203606, 4915200, "'", "cmr10", 9.96], - [10385651, 4915200, "s", "cmr10", 9.96], - [10862607, 4915200, "d", "cmr10", 9.96], - [11226697, 4915200, "v", "cmr10", 9.96], - [11572583, 4915200, "i", "cmr10", 9.96], - [11754628, 4915200, "r", "cmr10", 9.96], - [12011311, 4915200, "e", "cmr10", 9.96], - [12302582, 4915200, "a", "cmr10", 9.96], - [12630263, 4915200, "d", "cmr10", 9.96], - [13686591, 6629148, "\u0019", "cmmi5", 4.98], - [13717140, 6963172, "2", "cmr5", 4.98], - [13355327, 7035991, "Z", "cmex10", 9.96], - [13406754, 8897228, "0", "cmr7", 6.97], - [14010688, 7200560, "\u0010", "cmex10", 9.96], - [14937658, 7484660, "x", "cmmi10", 9.96], - [14480727, 8377560, "s", "cmr10", 9.96], - [14739230, 8377560, "i", "cmr10", 9.96], - [14921275, 8377560, "n", "cmr10", 9.96], - [15394589, 8377560, "x", "cmmi10", 9.96], - [15847788, 7200560, "\u0011", "cmex10", 9.96], - [16239184, 7336365, "2", "cmr7", 6.97], - [16642411, 7928016, "d", "cmr10", 9.96], - [17006501, 7928016, "x", "cmmi10", 9.96] - ], - "boxes": [ - [4063232, 5701632, 65536, 22609920], - [13686591, 6703682, 26213, 284106], - [14480727, 7777282, 26213, 1288418] - ] - }, - { - "text": [ - [5046272, 4128768, "a", "cmr10", 9.96], - [5373953, 4128768, "n", "cmr10", 9.96], - [5738043, 4128768, "o", "cmr10", 9.96], - [6065724, 4128768, "t", "cmr10", 9.96], - [6320587, 4128768, "h", "cmr10", 9.96], - [6684677, 4128768, "e", "cmr10", 9.96], - [6975948, 4128768, "r", "cmr10", 9.96], - [7451084, 4128768, "p", "cmr10", 9.96], - [7815174, 4128768, "a", "cmr10", 9.96], - [8142855, 4128768, "g", "cmr10", 9.96], - [8470536, 4128768, "e", "cmr10", 9.96] - ], - "boxes": [] - } -] diff --git a/lib/matplotlib/tests/baseline_images/dviread/test.tex b/lib/matplotlib/tests/baseline_images/dviread/test.tex index 33220fedae3e..4a2d4720c065 100644 --- a/lib/matplotlib/tests/baseline_images/dviread/test.tex +++ b/lib/matplotlib/tests/baseline_images/dviread/test.tex @@ -1,17 +1,19 @@ -% source file for test.dvi \documentclass{article} +\usepackage{iftex} +\iftutex\usepackage{fontspec}\fi % xetex or luatex \pagestyle{empty} + \begin{document} -This is a \LaTeX\ test document\\ -for testing matplotlib's dviread +A \LaTeX { + \iftutex\fontspec{DejaVuSans.ttf}[ + FakeSlant=0.25, FakeStretch=1.25, FakeBold=2.5, Color=0000FF]\fi + doc. +} -\noindent\rule{\textwidth}{1pt} +\newpage \[ \int\limits_0^{\frac{\pi}{2}} \Bigl(\frac{x}{\sin x}\Bigr)^2\,\mathrm{d}x \] \special{Special!} -\newpage -another page - \end{document} diff --git a/lib/matplotlib/tests/baseline_images/dviread/xelatex.json b/lib/matplotlib/tests/baseline_images/dviread/xelatex.json new file mode 100644 index 000000000000..8fb81ddf0c7e --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/dviread/xelatex.json @@ -0,0 +1 @@ +[{"text": [[5046272, 4128768, "A", "lmroman10-regular.otf", 9.96, {}], [5756027, 4128768, "L", "lmroman10-regular.otf", 9.96, {}], [5929697, 4012179, "A", "lmroman7-regular.otf", 6.97, {}], [6218125, 4128768, "T", "lmroman10-regular.otf", 9.96, {}], [6582045, 4269998, "E", "lmroman10-regular.otf", 9.96, {}], [6946425, 4128768, "X", "lmroman10-regular.otf", 9.96, {}], [7656180, 4128768, "d", "DejaVuSans.ttf", 9.96, {"rgba": [0, 0, 255, 255], "extend": 1.25, "slant": 0.25, "embolden": 0.25}], [8176180, 4128768, "o", "DejaVuSans.ttf", 9.96, {"rgba": [0, 0, 255, 255], "extend": 1.25, "slant": 0.25, "embolden": 0.25}], [8677380, 4128768, "c", "DejaVuSans.ttf", 9.96, {"rgba": [0, 0, 255, 255], "extend": 1.25, "slant": 0.25, "embolden": 0.25}], [9127780, 4128768, ".", "DejaVuSans.ttf", 9.96, {"rgba": [0, 0, 255, 255], "extend": 1.25, "slant": 0.25, "embolden": 0.25}]], "boxes": []}, {"text": [[13686374, 5056284, "\u03c0", "cmmi5.pfb", 4.98, {}], [13716923, 5390308, "2", "cmr5.pfb", 4.98, {}], [13355110, 5463127, "integraldisplay", "cmex10.pfb", 9.96, {}], [13406537, 7324364, "0", "cmr7.pfb", 6.97, {}], [14010471, 5627696, "parenleftBig", "cmex10.pfb", 9.96, {}], [14937513, 5911796, "x", "cmmi10.pfb", 9.96, {}], [14480510, 6804696, "s", "lmroman10-regular.otf", 9.96, {}], [14738722, 6804696, "i", "lmroman10-regular.otf", 9.96, {}], [14920912, 6804696, "n", "lmroman10-regular.otf", 9.96, {}], [15394516, 6804696, "x", "cmmi10.pfb", 9.96, {}], [15847715, 5627696, "parenrightBig", "cmex10.pfb", 9.96, {}], [16239111, 5763501, "2", "cmr7.pfb", 6.97, {}], [16642338, 6355152, "d", "lmroman10-regular.otf", 9.96, {}], [17006718, 6355152, "x", "cmmi10.pfb", 9.96, {}]], "boxes": [[13686374, 5130818, 26213, 284106], [14480510, 6204418, 26213, 1288562]]}] diff --git a/lib/matplotlib/tests/test_dviread.py b/lib/matplotlib/tests/test_dviread.py index 7b7ff151be18..7aecd1fc03ae 100644 --- a/lib/matplotlib/tests/test_dviread.py +++ b/lib/matplotlib/tests/test_dviread.py @@ -2,7 +2,8 @@ from pathlib import Path import shutil -import matplotlib.dviread as dr +from matplotlib import cbook, dviread as dr +from matplotlib.testing import subprocess_run_for_testing import pytest @@ -62,16 +63,45 @@ def test_PsfontsMap(monkeypatch): @pytest.mark.skipif(shutil.which("kpsewhich") is None, reason="kpsewhich is not available") -def test_dviread(): - dirpath = Path(__file__).parent / 'baseline_images/dviread' - with (dirpath / 'test.json').open() as f: - correct = json.load(f) - with dr.Dvi(str(dirpath / 'test.dvi'), None) as dvi: - data = [{'text': [[t.x, t.y, - chr(t.glyph), - t.font.texname.decode('ascii'), - round(t.font.size, 2)] - for t in page.text], - 'boxes': [[b.x, b.y, b.height, b.width] for b in page.boxes]} - for page in dvi] +@pytest.mark.parametrize("engine", ["pdflatex", "xelatex", "lualatex"]) +def test_dviread(tmp_path, engine, monkeypatch): + dirpath = Path(__file__).parent / "baseline_images/dviread" + shutil.copy(dirpath / "test.tex", tmp_path) + shutil.copy(cbook._get_data_path("fonts/ttf/DejaVuSans.ttf"), tmp_path) + cmd, fmt = { + "pdflatex": (["latex"], "dvi"), + "xelatex": (["xelatex", "-no-pdf"], "xdv"), + "lualatex": (["lualatex", "-output-format=dvi"], "dvi"), + }[engine] + if shutil.which(cmd[0]) is None: + pytest.skip(f"{cmd[0]} is not available") + subprocess_run_for_testing( + [*cmd, "test.tex"], cwd=tmp_path, check=True, capture_output=True) + # dviread must be run from the tmppath directory because {xe,lua}tex output + # records the path to DejaVuSans.ttf as it is written in the tex source, + # i.e. as a relative path. + monkeypatch.chdir(tmp_path) + with dr.Dvi(tmp_path / f"test.{fmt}", None) as dvi: + try: + pages = [*dvi] + except FileNotFoundError as exc: + for note in getattr(exc, "__notes__", []): + if "too-old version of luaotfload" in note: + pytest.skip(note) + raise + data = [ + { + "text": [ + [ + t.x, t.y, + t._as_unicode_or_name(), + t.font.resolve_path().name, + round(t.font.size, 2), + t.font.effects, + ] for t in page.text + ], + "boxes": [[b.x, b.y, b.height, b.width] for b in page.boxes] + } for page in pages + ] + correct = json.loads((dirpath / f"{engine}.json").read_text()) assert data == correct diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index b57597ded363..8deae19c42e7 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -234,7 +234,9 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, # characters into strings. t1_encodings = {} for text in page.text: - font = get_font(text.font_path) + font = get_font(text.font.resolve_path()) + if text.font.subfont: + raise NotImplementedError("Indexing TTC fonts is not supported yet") char_id = self._get_char_id(font, text.glyph) if char_id not in glyph_map: font.clear() From ba01ff44490c3b553c5443f3bc21002c1a66202f Mon Sep 17 00:00:00 2001 From: Ruth Comer <10599679+rcomer@users.noreply.github.com> Date: Wed, 3 Sep 2025 13:28:17 +0100 Subject: [PATCH 076/110] Update syntax for PR welcome workflow It looks like the syntax for this changed at https://github.com/actions/first-interaction/pull/311 --- .github/workflows/pr_welcome.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr_welcome.yml b/.github/workflows/pr_welcome.yml index 0a654753861a..874f8807b478 100644 --- a/.github/workflows/pr_welcome.yml +++ b/.github/workflows/pr_welcome.yml @@ -11,8 +11,8 @@ jobs: steps: - uses: actions/first-interaction@753c925c8d1ac6fede23781875376600628d9b5d # v3.0.0 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - pr-message: >+ + repo_token: ${{ secrets.GITHUB_TOKEN }} + pr_message: >+ Thank you for opening your first PR into Matplotlib! From 628a68f122d5e7252bcb6d0c9d9bd910049827ad Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 29 Aug 2025 23:58:54 -0400 Subject: [PATCH 077/110] TST: Use a temporary directory for test_save_figure_return This avoids having to manually clean up the resulting file, which seems flaky. --- lib/matplotlib/tests/test_backend_qt.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index 3f34a58a765d..cbe2e9a5264c 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -215,14 +215,15 @@ def test_figureoptions(): @pytest.mark.backend('QtAgg', skip_on_importerror=True) -def test_save_figure_return(): +def test_save_figure_return(tmp_path): fig, ax = plt.subplots() ax.imshow([[1]]) + expected = tmp_path / "foobar.png" prop = "matplotlib.backends.qt_compat.QtWidgets.QFileDialog.getSaveFileName" - with mock.patch(prop, return_value=("foobar.png", None)): + with mock.patch(prop, return_value=(str(expected), None)): fname = fig.canvas.manager.toolbar.save_figure() - os.remove("foobar.png") - assert fname == "foobar.png" + assert fname == str(expected) + assert expected.exists() with mock.patch(prop, return_value=(None, None)): fname = fig.canvas.manager.toolbar.save_figure() assert fname is None From f791853952e93c6609d61a08c0e271a287c77ffe Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 2 Sep 2025 23:45:39 +0200 Subject: [PATCH 078/110] DOC: Clarify draft PR and move from ways to contribute to PR guidelines Closes #30436. --- doc/devel/contribute.rst | 16 +++++++--------- doc/devel/development_workflow.rst | 6 +++--- doc/devel/pr_guide.rst | 18 +++++++++++++++++- 3 files changed, 27 insertions(+), 13 deletions(-) diff --git a/doc/devel/contribute.rst b/doc/devel/contribute.rst index 558e19790d82..e2291e3255e6 100644 --- a/doc/devel/contribute.rst +++ b/doc/devel/contribute.rst @@ -29,7 +29,8 @@ Ways to contribute * **You are a Matplotlib user, and you see a bug, a potential improvement, or something that annoys you, and you can fix it.** - You can search our issue tracker for an existing issue that describes your problem or + You can search our `issue tracker `__ + for an existing issue that describes your problem or open a new issue to inform us of the problem you observed and discuss the best approach to fix it. If your contributions would not be captured on GitHub (social media, communication, educational content), you can also reach out to us on gitter_, @@ -42,14 +43,11 @@ Ways to contribute Awesome — you have a focus on a specific application and domain and can start there. In this case, maintainers can help you figure out the best - implementation; open an issue or pull request with a starting point, and we'll - be happy to discuss technical approaches. + implementation; `open an issue `__ + in our issue tracker, and we'll be happy to discuss technical approaches. - If you prefer, you can use the `GitHub functionality for "draft" pull requests - `__ - and request early feedback on whatever you are working on, but you should be - aware that maintainers may not review your contribution unless it has the - "Ready to review" state on GitHub. + If you can implement the solution yourself, even better! Consider contributing + the change as a :ref:`pull request ` right away. * **You are new to Matplotlib, both as a user and contributor, and want to start contributing but have yet to develop a particular interest.** @@ -287,7 +285,7 @@ guide you through each step: 4. Check existing pull requests (e.g., :ghpull:`28476`) and filter by the issue number to make sure the issue is not in progress: * If the issue has a pull request (is in progress), tag the user working on the issue, and ask to collaborate (optional). - * If a pull request does not exist, create a `draft pull request `_ and follow the `pull request guidelines `_. + * If there is no pull request, :ref:`create a new pull request `. 5. Please familiarize yourself with the pull request template (see below), and ensure you understand/are able to complete the template when you open your pull request. Additional information can be found in the `pull request guidelines `_. diff --git a/doc/devel/development_workflow.rst b/doc/devel/development_workflow.rst index 16766278f658..c0300acf1f7f 100644 --- a/doc/devel/development_workflow.rst +++ b/doc/devel/development_workflow.rst @@ -179,9 +179,9 @@ Enter a title for the set of changes with some explanation of what you've done. Mention anything you'd like particular attention for - such as a complicated change or some code you are not happy with. -If you don't think your request is ready to be merged, just say so in your pull -request message and use the "Draft PR" feature of GitHub. This is a good way of -getting some preliminary code review. +If you don't think your request is ready to be merged, make a +:ref:`draft pull request ` and state what aspects you want to have +feedback on. This is a good way of getting some preliminary code review. For more guidance on the mechanics of making a pull request, see GitHub's `pull request tutorial `_. diff --git a/doc/devel/pr_guide.rst b/doc/devel/pr_guide.rst index a02b52ad5a38..2dc4e34dd6fe 100644 --- a/doc/devel/pr_guide.rst +++ b/doc/devel/pr_guide.rst @@ -12,7 +12,7 @@ We value contributions from people with all levels of experience. In particular, if this is your first PR not everything has to be perfect. We'll guide you through the PR process. Nevertheless, please try to follow our guidelines as well as you can to help make the PR process quick and smooth. If your pull request is -incomplete or a work-in-progress, please mark it as a `draft pull requests `_ +incomplete or a work-in-progress, please mark it as a :ref:`draft pull request ` on GitHub and specify what feedback from the developers would be helpful. Please be patient with reviewers. We try our best to respond quickly, but we have @@ -118,6 +118,22 @@ Workflow Detailed guidelines =================== +.. _draft-pr: + +Draft PRs +--------- + +Authors may create a `draft PR`_ (or change to draft status later) if the code +is not yet ready for a regular full review. Typical use cases are posting code +as a basis for discussion or signalling that you intend to rework the code as +a result of feedback. Authors should clearly communicate why the PR has draft +status and what needs to be done to make it ready for review. In particular, +they should explicitly ask for targeted feedback if needed. By default, +reviewers will not look at the code of a draft PR and only respond to specific +questions by the author. + +.. _draft PR: https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests#draft-pull-requests + .. _pr-documentation: Documentation From 6af9062f0aa178c5a951854351585ea475038d6c Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 7 Sep 2025 09:42:56 +0200 Subject: [PATCH 079/110] Add note on incompatibility with blitting of widgets --- doc/release/next_whats_new/pyplot-register-figure.rst | 3 +++ lib/matplotlib/tests/test_backends_interactive.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/doc/release/next_whats_new/pyplot-register-figure.rst b/doc/release/next_whats_new/pyplot-register-figure.rst index 6df2676e0ffa..c619aa428695 100644 --- a/doc/release/next_whats_new/pyplot-register-figure.rst +++ b/doc/release/next_whats_new/pyplot-register-figure.rst @@ -58,3 +58,6 @@ general, you should not store a reference to the canvas, but rather always obtain it from the figure with ``fig.canvas``. This will return the current canvas, which is either the original `.FigureCanvasBase` or a backend-dependent subclass, depending on whether the figure is registered with pyplot or not. +Additionally, the swapping of the canvas currently does not play well with +blitting of matplotlib widgets; in such cases either deactivate blitting or do not +continue to use the figure (e.g. saving it after closing the window). diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 671ad8466aee..d7069cd5377f 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -18,6 +18,7 @@ import matplotlib as mpl from matplotlib import _c_internal_utils +from matplotlib.backend_bases import FigureCanvasBase from matplotlib.backend_tools import ToolToggleBase from matplotlib.testing import subprocess_run_helper as _run_helper, is_ci_environment @@ -229,6 +230,7 @@ def check_alt_backend(alt_backend): # When the figure is closed, it's manager is removed and the canvas is reset to # FigureCanvasBase. Saving should still be possible. + assert type(fig.canvas) == FigureCanvasBase result_after = io.BytesIO() fig.savefig(result_after, format='png', dpi=100) From 95773bb204214056a2dfe0a279a4ec7eb088cb55 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 19:29:58 +0000 Subject: [PATCH 080/110] Bump the actions group across 1 directory with 10 updates Bumps the actions group with 10 updates in the / directory: | Package | From | To | | --- | --- | --- | | [actions/setup-python](https://github.com/actions/setup-python) | `5.6.0` | `6.0.0` | | [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) | `3.1.3` | `3.1.4` | | [actions/attest-build-provenance](https://github.com/actions/attest-build-provenance) | `2.4.0` | `3.0.0` | | [pypa/gh-action-pypi-publish](https://github.com/pypa/gh-action-pypi-publish) | `1.12.4` | `1.13.0` | | [reviewdog/action-setup](https://github.com/reviewdog/action-setup) | `1.3.2` | `1.4.0` | | [github/codeql-action](https://github.com/github/codeql-action) | `3.29.10` | `3.30.1` | | [actions/labeler](https://github.com/actions/labeler) | `5.0.0` | `6.0.1` | | [reviewdog/action-eslint](https://github.com/reviewdog/action-eslint) | `1.33.2` | `1.34.0` | | [actions/stale](https://github.com/actions/stale) | `9.1.0` | `10.0.0` | | [codecov/codecov-action](https://github.com/codecov/codecov-action) | `5.4.3` | `5.5.1` | Updates `actions/setup-python` from 5.6.0 to 6.0.0 - [Release notes](https://github.com/actions/setup-python/releases) - [Commits](https://github.com/actions/setup-python/compare/a26af69be951a213d495a4c3e4e4022e16d87065...e797f83bcb11b83ae66e0230d6156d7c80228e7c) Updates `pypa/cibuildwheel` from 3.1.3 to 3.1.4 - [Release notes](https://github.com/pypa/cibuildwheel/releases) - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) - [Commits](https://github.com/pypa/cibuildwheel/compare/352e01339f0a173aa2a3eb57f01492e341e83865...c923d83ad9c1bc00211c5041d0c3f73294ff88f6) Updates `actions/attest-build-provenance` from 2.4.0 to 3.0.0 - [Release notes](https://github.com/actions/attest-build-provenance/releases) - [Changelog](https://github.com/actions/attest-build-provenance/blob/main/RELEASE.md) - [Commits](https://github.com/actions/attest-build-provenance/compare/e8998f949152b193b063cb0ec769d69d929409be...977bb373ede98d70efdf65b84cb5f73e068dcc2a) Updates `pypa/gh-action-pypi-publish` from 1.12.4 to 1.13.0 - [Release notes](https://github.com/pypa/gh-action-pypi-publish/releases) - [Commits](https://github.com/pypa/gh-action-pypi-publish/compare/76f52bc884231f62b9a034ebfe128415bbaabdfc...ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e) Updates `reviewdog/action-setup` from 1.3.2 to 1.4.0 - [Release notes](https://github.com/reviewdog/action-setup/releases) - [Commits](https://github.com/reviewdog/action-setup/compare/e04ffabe3898a0af8d0fb1af00c188831c4b5893...d8edfce3dd5e1ec6978745e801f9c50b5ef80252) Updates `github/codeql-action` from 3.29.10 to 3.30.1 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/96f518a34f7a870018057716cc4d7a5c014bd61c...f1f6e5f6af878fb37288ce1c627459e94dbf7d01) Updates `actions/labeler` from 5.0.0 to 6.0.1 - [Release notes](https://github.com/actions/labeler/releases) - [Commits](https://github.com/actions/labeler/compare/8558fd74291d67161a8a78ce36a881fa63b766a9...634933edcd8ababfe52f92936142cc22ac488b1b) Updates `reviewdog/action-eslint` from 1.33.2 to 1.34.0 - [Release notes](https://github.com/reviewdog/action-eslint/releases) - [Commits](https://github.com/reviewdog/action-eslint/compare/2fee6dd72a5419ff4113f694e2068d2a03bb35dd...556a3fdaf8b4201d4d74d406013386aa4f7dab96) Updates `actions/stale` from 9.1.0 to 10.0.0 - [Release notes](https://github.com/actions/stale/releases) - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/stale/compare/5bef64f19d7facfb25b37b414482c7164d639639...3a9db7e6a41a89f618792c92c0e97cc736e1b13f) Updates `codecov/codecov-action` from 5.4.3 to 5.5.1 - [Release notes](https://github.com/codecov/codecov-action/releases) - [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/codecov/codecov-action/compare/18283e04ce6e62d37312384ff67231eb8fd56d24...5a1091511ad55cbe89839c7260b706298ca349f7) --- updated-dependencies: - dependency-name: actions/setup-python dependency-version: 6.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: pypa/cibuildwheel dependency-version: 3.1.4 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions - dependency-name: actions/attest-build-provenance dependency-version: 3.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: pypa/gh-action-pypi-publish dependency-version: 1.13.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: reviewdog/action-setup dependency-version: 1.4.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: github/codeql-action dependency-version: 3.30.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: actions/labeler dependency-version: 6.0.1 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: reviewdog/action-eslint dependency-version: 1.34.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions - dependency-name: actions/stale dependency-version: 10.0.0 dependency-type: direct:production update-type: version-update:semver-major dependency-group: actions - dependency-name: codecov/codecov-action dependency-version: 5.5.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/cibuildwheel.yml | 16 ++++++++-------- .github/workflows/circleci.yml | 2 +- .github/workflows/codeql-analysis.yml | 4 ++-- .github/workflows/labeler.yml | 2 +- .github/workflows/linting.yml | 12 ++++++------ .github/workflows/mypy-stubtest.yml | 4 ++-- .github/workflows/stale-tidy.yml | 2 +- .github/workflows/stale.yml | 2 +- .github/workflows/tests.yml | 4 ++-- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 9d4de069b078..8ec11e30e122 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -46,7 +46,7 @@ jobs: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 name: Install Python with: python-version: '3.11' @@ -143,7 +143,7 @@ jobs: path: dist/ - name: Build wheels for CPython 3.14 - uses: pypa/cibuildwheel@352e01339f0a173aa2a3eb57f01492e341e83865 # v3.1.3 + uses: pypa/cibuildwheel@c923d83ad9c1bc00211c5041d0c3f73294ff88f6 # v3.1.4 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -153,7 +153,7 @@ jobs: CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 - name: Build wheels for CPython 3.13 - uses: pypa/cibuildwheel@352e01339f0a173aa2a3eb57f01492e341e83865 # v3.1.3 + uses: pypa/cibuildwheel@c923d83ad9c1bc00211c5041d0c3f73294ff88f6 # v3.1.4 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -162,7 +162,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.12 - uses: pypa/cibuildwheel@352e01339f0a173aa2a3eb57f01492e341e83865 # v3.1.3 + uses: pypa/cibuildwheel@c923d83ad9c1bc00211c5041d0c3f73294ff88f6 # v3.1.4 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -170,7 +170,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.11 - uses: pypa/cibuildwheel@352e01339f0a173aa2a3eb57f01492e341e83865 # v3.1.3 + uses: pypa/cibuildwheel@c923d83ad9c1bc00211c5041d0c3f73294ff88f6 # v3.1.4 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -178,7 +178,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for PyPy - uses: pypa/cibuildwheel@352e01339f0a173aa2a3eb57f01492e341e83865 # v3.1.3 + uses: pypa/cibuildwheel@c923d83ad9c1bc00211c5041d0c3f73294ff88f6 # v3.1.4 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -215,9 +215,9 @@ jobs: run: ls dist - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 + uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3.0.0 with: subject-path: dist/matplotlib-* - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 diff --git a/.github/workflows/circleci.yml b/.github/workflows/circleci.yml index 3838a38004e0..9a2516efd4bb 100644 --- a/.github/workflows/circleci.yml +++ b/.github/workflows/circleci.yml @@ -41,7 +41,7 @@ jobs: - name: Set up reviewdog if: "${{ steps.fetch-artifacts.outputs.count != 0 }}" - uses: reviewdog/action-setup@e04ffabe3898a0af8d0fb1af00c188831c4b5893 # v1.3.2 + uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.4.0 with: reviewdog_version: latest diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index eebdd65105e3..07c9c1701cf1 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,7 +32,7 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@96f518a34f7a870018057716cc4d7a5c014bd61c # v3.29.10 + uses: github/codeql-action/init@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 with: languages: ${{ matrix.language }} @@ -43,4 +43,4 @@ jobs: pip install --user -v . - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@96f518a34f7a870018057716cc4d7a5c014bd61c # v3.29.10 + uses: github/codeql-action/analyze@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 8e2002353164..17c4922df054 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -10,6 +10,6 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 + - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 with: sync-labels: true diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index f5cada1f3f9d..6878f2dac8b3 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -14,7 +14,7 @@ jobs: with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: "3.x" - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 @@ -32,7 +32,7 @@ jobs: persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: '3.11' @@ -40,7 +40,7 @@ jobs: run: pip3 install ruff - name: Set up reviewdog - uses: reviewdog/action-setup@e04ffabe3898a0af8d0fb1af00c188831c4b5893 # v1.3.9 + uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.3.9 - name: Run ruff env: @@ -61,7 +61,7 @@ jobs: persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: '3.11' @@ -69,7 +69,7 @@ jobs: run: pip3 install -r requirements/testing/mypy.txt -r requirements/testing/all.txt - name: Set up reviewdog - uses: reviewdog/action-setup@e04ffabe3898a0af8d0fb1af00c188831c4b5893 # v1.3.9 + uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.3.9 - name: Run mypy env: @@ -92,7 +92,7 @@ jobs: persist-credentials: false - name: eslint - uses: reviewdog/action-eslint@2fee6dd72a5419ff4113f694e2068d2a03bb35dd # v1.33.2 + uses: reviewdog/action-eslint@556a3fdaf8b4201d4d74d406013386aa4f7dab96 # v1.34.0 with: filter_mode: nofilter github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/mypy-stubtest.yml b/.github/workflows/mypy-stubtest.yml index b40909b371a6..9420f5e43ea7 100644 --- a/.github/workflows/mypy-stubtest.yml +++ b/.github/workflows/mypy-stubtest.yml @@ -17,12 +17,12 @@ jobs: persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: '3.11' - name: Set up reviewdog - uses: reviewdog/action-setup@e04ffabe3898a0af8d0fb1af00c188831c4b5893 # v1.3.9 + uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.3.9 - name: Install tox run: python -m pip install tox diff --git a/.github/workflows/stale-tidy.yml b/.github/workflows/stale-tidy.yml index bc50dc892155..09b9cc49a8f8 100644 --- a/.github/workflows/stale-tidy.yml +++ b/.github/workflows/stale-tidy.yml @@ -9,7 +9,7 @@ jobs: if: github.repository == 'matplotlib/matplotlib' runs-on: ubuntu-latest steps: - - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} operations-per-run: 300 diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b65b44a59e88..624792ed171a 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -9,7 +9,7 @@ jobs: if: github.repository == 'matplotlib/matplotlib' runs-on: ubuntu-latest steps: - - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + - uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0 with: repo-token: ${{ secrets.GITHUB_TOKEN }} operations-per-run: 20 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e965819628be..c6f1e25c1e92 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -96,7 +96,7 @@ jobs: persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -396,7 +396,7 @@ jobs: fi - name: Upload code coverage if: ${{ !cancelled() && github.event_name != 'schedule' }} - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1 with: name: "${{ matrix.python-version }} ${{ matrix.os }} ${{ matrix.name-suffix }}" token: ${{ secrets.CODECOV_TOKEN }} From 6b1797b27ef8c95c2b5f0174c527fe5a0dd2dd12 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:49:06 +0200 Subject: [PATCH 081/110] Fix assertion error import must be local --- lib/matplotlib/tests/test_backends_interactive.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index d7069cd5377f..18731ab3fa0b 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -18,7 +18,6 @@ import matplotlib as mpl from matplotlib import _c_internal_utils -from matplotlib.backend_bases import FigureCanvasBase from matplotlib.backend_tools import ToolToggleBase from matplotlib.testing import subprocess_run_helper as _run_helper, is_ci_environment @@ -163,7 +162,7 @@ def _test_interactive_impl(): import matplotlib as mpl from matplotlib import pyplot as plt - from matplotlib.backend_bases import KeyEvent + from matplotlib.backend_bases import KeyEvent, FigureCanvasBase mpl.rcParams.update({ "webagg.open_in_browser": False, "webagg.port_retries": 1, @@ -230,7 +229,7 @@ def check_alt_backend(alt_backend): # When the figure is closed, it's manager is removed and the canvas is reset to # FigureCanvasBase. Saving should still be possible. - assert type(fig.canvas) == FigureCanvasBase + assert type(fig.canvas) == FigureCanvasBase, str(fig.canvas) result_after = io.BytesIO() fig.savefig(result_after, format='png', dpi=100) From b42ce647ea4bff5cd4be77462f66bb1c71a8c06e Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Mon, 8 Sep 2025 22:37:20 +0200 Subject: [PATCH 082/110] Fix macosx --- src/_macosx.m | 1 + 1 file changed, 1 insertion(+) diff --git a/src/_macosx.m b/src/_macosx.m index 1372157bc80d..a8f91266c676 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -686,6 +686,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) { [self->window close]; self->window = NULL; + [super destroy]; Py_RETURN_NONE; } From 2dd81752c8a8a26f8d67a7f322bea78b9a0a2104 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Mon, 8 Sep 2025 23:59:38 +0200 Subject: [PATCH 083/110] MNT: Change default name of ListedColormaps from "from_list" to "unnamed", which makes more sense logically. While technically a breaking change, I anticipate that nobody is relying on that default value. --- doc/api/next_api_changes/behavior/30532-TH.rst | 4 ++++ lib/matplotlib/colors.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 doc/api/next_api_changes/behavior/30532-TH.rst diff --git a/doc/api/next_api_changes/behavior/30532-TH.rst b/doc/api/next_api_changes/behavior/30532-TH.rst new file mode 100644 index 000000000000..62bf481e1b75 --- /dev/null +++ b/doc/api/next_api_changes/behavior/30532-TH.rst @@ -0,0 +1,4 @@ +Default name of ``ListedColormap`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The default name of `ListedColormap` has changed from "from_list" to "unnamed". diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index f60c8eb48134..679f368bae30 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -1322,7 +1322,7 @@ class ListedColormap(Colormap): "and will be removed in %(removal)s. Please ensure the list " "of passed colors is the required length instead." ) - def __init__(self, colors, name='from_list', N=None, *, + def __init__(self, colors, name='unnamed', N=None, *, bad=None, under=None, over=None): if N is None: self.colors = colors From 918f7ee93bdf25ec7b8dbf2220fb05d3f304a885 Mon Sep 17 00:00:00 2001 From: LangQi99 <2032771946@qq.com> Date: Tue, 9 Sep 2025 10:05:46 +0800 Subject: [PATCH 084/110] fix import warning --- lib/matplotlib/backends/backend_gtk3.py | 2 ++ lib/matplotlib/backends/backend_gtk4.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 888f5a770f5d..38c1541b8df1 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -18,6 +18,8 @@ # :raises ValueError: If module/version is already loaded, already # required, or unavailable. gi.require_version("Gtk", "3.0") + # Also require GioUnix to avoid PyGIWarning when Gio is imported + gi.require_version("GioUnix", "2.0") except ValueError as e: # in this case we want to re-raise as ImportError so the # auto-backend selection logic correctly skips. diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index cd38968779ed..ec45c1f5b205 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -17,6 +17,8 @@ # :raises ValueError: If module/version is already loaded, already # required, or unavailable. gi.require_version("Gtk", "4.0") + # Also require GioUnix to avoid PyGIWarning when Gio is imported + gi.require_version("GioUnix", "2.0") except ValueError as e: # in this case we want to re-raise as ImportError so the # auto-backend selection logic correctly skips. From 2bef93f32f15fcfad77de3c8530731d84f7516cf Mon Sep 17 00:00:00 2001 From: LangQi99 <2032771946@qq.com> Date: Tue, 9 Sep 2025 10:33:53 +0800 Subject: [PATCH 085/110] fix --- lib/matplotlib/backends/backend_gtk3.py | 7 ++++++- lib/matplotlib/backends/backend_gtk4.py | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 38c1541b8df1..4e05119aa0f6 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -19,7 +19,12 @@ # required, or unavailable. gi.require_version("Gtk", "3.0") # Also require GioUnix to avoid PyGIWarning when Gio is imported - gi.require_version("GioUnix", "2.0") + # GioUnix is platform-specific and may not be available on all systems + try: + gi.require_version("GioUnix", "2.0") + except ValueError: + # GioUnix is not available on this platform, which is fine + pass except ValueError as e: # in this case we want to re-raise as ImportError so the # auto-backend selection logic correctly skips. diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index ec45c1f5b205..a45fa0bc490f 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -18,7 +18,12 @@ # required, or unavailable. gi.require_version("Gtk", "4.0") # Also require GioUnix to avoid PyGIWarning when Gio is imported - gi.require_version("GioUnix", "2.0") + # GioUnix is platform-specific and may not be available on all systems + try: + gi.require_version("GioUnix", "2.0") + except ValueError: + # GioUnix is not available on this platform, which is fine + pass except ValueError as e: # in this case we want to re-raise as ImportError so the # auto-backend selection logic correctly skips. From c6210d85b2b9c24032eec18f8e77daac18d2106d Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 9 Sep 2025 07:00:02 +0200 Subject: [PATCH 086/110] Update doc/api/next_api_changes/behavior/30532-TH.rst Co-authored-by: Elliott Sales de Andrade --- doc/api/next_api_changes/behavior/30532-TH.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/api/next_api_changes/behavior/30532-TH.rst b/doc/api/next_api_changes/behavior/30532-TH.rst index 62bf481e1b75..3d368c566039 100644 --- a/doc/api/next_api_changes/behavior/30532-TH.rst +++ b/doc/api/next_api_changes/behavior/30532-TH.rst @@ -1,4 +1,4 @@ Default name of ``ListedColormap`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The default name of `ListedColormap` has changed from "from_list" to "unnamed". +The default name of `.ListedColormap` has changed from "from_list" to "unnamed". From 56911722d53aa8ddd58b8a123b89839de0193e19 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 9 Sep 2025 11:49:01 +0200 Subject: [PATCH 087/110] Fix reviewdog version number --- .github/workflows/linting.yml | 4 ++-- .github/workflows/mypy-stubtest.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 6878f2dac8b3..f1c6d21019e3 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -40,7 +40,7 @@ jobs: run: pip3 install ruff - name: Set up reviewdog - uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.3.9 + uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.4.0 - name: Run ruff env: @@ -69,7 +69,7 @@ jobs: run: pip3 install -r requirements/testing/mypy.txt -r requirements/testing/all.txt - name: Set up reviewdog - uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.3.9 + uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.4.0 - name: Run mypy env: diff --git a/.github/workflows/mypy-stubtest.yml b/.github/workflows/mypy-stubtest.yml index 9420f5e43ea7..3815efd08954 100644 --- a/.github/workflows/mypy-stubtest.yml +++ b/.github/workflows/mypy-stubtest.yml @@ -22,7 +22,7 @@ jobs: python-version: '3.11' - name: Set up reviewdog - uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.3.9 + uses: reviewdog/action-setup@d8edfce3dd5e1ec6978745e801f9c50b5ef80252 # v1.4.0 - name: Install tox run: python -m pip install tox From 3b29f6d5089bae938fbe299379ad9ea63a3c3f43 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 9 Sep 2025 12:12:45 +0200 Subject: [PATCH 088/110] DOC: Cleanup/restructure PR guidelines - move contents from "Approval" into new section "Merging" - move contents from "Number of commits and squashing" into new section "Review" - Update "Summary for pull request reviewers" accordingly --- doc/devel/pr_guide.rst | 55 ++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 23 deletions(-) diff --git a/doc/devel/pr_guide.rst b/doc/devel/pr_guide.rst index 2dc4e34dd6fe..e7e3ceba8f95 100644 --- a/doc/devel/pr_guide.rst +++ b/doc/devel/pr_guide.rst @@ -109,9 +109,10 @@ Workflow * The PR should :ref:`target the main branch `. * Tag with descriptive :ref:`labels `. * Set the :ref:`milestone `. -* Keep an eye on the :ref:`number of commits `. +* :ref:`Review ` the contents. * Approve if all of the above topics are handled. -* :ref:`Merge ` if a sufficient number of approvals is reached. +* Keep an eye on the :ref:`number of commits `. +* :ref:`Merge ` if a :ref:`sufficient number of approvals ` is reached. .. _pr-guidelines-details: @@ -190,10 +191,27 @@ All Pull Requests should target the main branch. The milestone tag triggers an :ref:`automatic backport ` for milestones which have a corresponding branch. -.. _pr-merging: +.. _pr-review: -Merging -------- +Review +------ + +* Do not let perfect be the enemy of the good, particularly for + documentation or example PRs. If you find yourself making many + small suggestions, either open a PR against the original branch, + push changes to the contributor branch, or merge the PR and then + open a new PR against upstream. + +* If you push to a contributor branch leave a comment explaining what + you did, ex "I took the liberty of pushing a small clean-up PR to + your branch, thanks for your work.". If you are going to make + substantial changes to the code or intent of the PR please check + with the contributor first. + +.. _pr-approval: + +Approval +-------- As a guiding principle, we require two `approvals`_ from core developers (those with commit rights) before merging a pull request. This two-pairs-of-eyes strategy shall ensure a consistent project direction and prevent accidental @@ -229,11 +247,6 @@ Some explicit rules following from this: A core dev should only champion one PR at a time and we should try to keep the flow of championed PRs reasonable. -After giving the last required approval, the author of the approval should -merge the PR. PR authors should not self-merge except for when another reviewer -explicitly allows it (e.g., "Approve modulo CI passing, may self merge when -green", or "Take or leave the comments. You may self merge".). - .. _pr-automated-tests: Automated tests @@ -241,6 +254,15 @@ Automated tests Before being merged, a PR should pass the :ref:`automated-tests`. If you are unsure why a test is failing, ask on the PR or in our :ref:`communication-channels` +.. _pr-merging: + +Merging +------- +After giving the last required :ref:`approval `, the author of the +approval should merge the PR. PR authors should not self-merge except for when +another reviewer explicitly allows it (e.g., "Approve modulo CI passing, may +self-merge when green", or "Take or leave the comments. You may self merge".). + .. _pr-squashing: Number of commits and squashing @@ -252,19 +274,6 @@ Number of commits and squashing about it is to eliminate binary files (ex multiple test image re-generations) and to remove upstream merges. -* Do not let perfect be the enemy of the good, particularly for - documentation or example PRs. If you find yourself making many - small suggestions, either open a PR against the original branch, - push changes to the contributor branch, or merge the PR and then - open a new PR against upstream. - -* If you push to a contributor branch leave a comment explaining what - you did, ex "I took the liberty of pushing a small clean-up PR to - your branch, thanks for your work.". If you are going to make - substantial changes to the code or intent of the PR please check - with the contributor first. - - .. _branches_and_backports: Branches and backports From 774d02c038c359d688672b1fd36c95ce7991e8aa Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Tue, 9 Sep 2025 21:46:36 +0200 Subject: [PATCH 089/110] Call super().destroy() in _maxosx.m FigureManager --- src/_macosx.m | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/_macosx.m b/src/_macosx.m index a8f91266c676..1dbd52bb0a14 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -572,6 +572,8 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) }, }; +static PyTypeObject FigureManagerType; + typedef struct { PyObject_HEAD Window* window; @@ -686,7 +688,24 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) { [self->window close]; self->window = NULL; - [super destroy]; + + // call super().destroy() + PyObject *super_obj = PyObject_CallFunctionObjArgs( + (PyObject *)&PySuper_Type, + (PyObject *)&FigureManagerType, + self, + NULL + ); + if (super_obj == NULL) { + return NULL; // error + } + PyObject *result = PyObject_CallMethod(super_obj, "destroy", NULL); + Py_DECREF(super_obj); + if (result == NULL) { + return NULL; // error + } + Py_DECREF(result); + Py_RETURN_NONE; } From 289c2d9352523e896a0734f4defdd5a48a9e4fb4 Mon Sep 17 00:00:00 2001 From: Tim Heap Date: Wed, 10 Sep 2025 15:16:17 +1000 Subject: [PATCH 090/110] Fix scale_unit/scale_units typo in quiver docs The example was missing the *s*, so copy+pasting the example gave an error when plotting. --- lib/matplotlib/quiver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index 91c510ca7060..df693c57d272 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -144,7 +144,7 @@ length in y direction = $\\frac{v}{\\mathrm{scale}} \\mathrm{scale_unit}$ - For example, ``(u, v) = (0.5, 0)`` with ``scale=10, scale_unit="width"`` results + For example, ``(u, v) = (0.5, 0)`` with ``scale=10, scale_units="width"`` results in a horizontal arrow with a length of *0.5 / 10 * "width"*, i.e. 0.05 times the Axes width. From f2a768287beffd25f5efa761f8a42653af01593f Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Wed, 10 Sep 2025 10:01:14 +0200 Subject: [PATCH 091/110] ENH: Scroll to zoom (#30405) * ENH: Scroll to zoom Implements a minimal version of #20317, in particular https://github .com/matplotlib/matplotlib/pull/20317#issuecomment-2233156558: Co-authored-by: Elliott Sales de Andrade --- doc/release/next_whats_new/scroll_to_zoom.rst | 8 ++++ lib/matplotlib/backend_bases.py | 48 +++++++++++++++++++ lib/matplotlib/backend_bases.pyi | 6 +++ 3 files changed, 62 insertions(+) create mode 100644 doc/release/next_whats_new/scroll_to_zoom.rst diff --git a/doc/release/next_whats_new/scroll_to_zoom.rst b/doc/release/next_whats_new/scroll_to_zoom.rst new file mode 100644 index 000000000000..bafa312c32a5 --- /dev/null +++ b/doc/release/next_whats_new/scroll_to_zoom.rst @@ -0,0 +1,8 @@ +Zooming using mouse wheel +~~~~~~~~~~~~~~~~~~~~~~~~~ + +``Ctrl+MouseWheel`` can be used to zoom in the plot windows. + +The zoom focusses on the mouse pointer, and keeps the aspect ratio of the axes. + +Zooming is currently only supported on rectilinear Axes. diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 8107471955fe..7560db80d2c1 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2574,6 +2574,51 @@ def button_press_handler(event, canvas=None, toolbar=None): toolbar.forward() +def scroll_handler(event, canvas=None, toolbar=None): + ax = event.inaxes + if ax is None: + return + if ax.name != "rectilinear": + # zooming is currently only supported on rectilinear axes + return + + if toolbar is None: + toolbar = (canvas or event.canvas).toolbar + + if toolbar is None: + # technically we do not need a toolbar, but until wheel zoom was + # introduced, any interactive modification was only possible through + # the toolbar tools. For now, we keep the restriction that a toolbar + # is required for interactive navigation. + return + + if event.key == "control": # zoom towards the mouse position + toolbar.push_current() + + xmin, xmax = ax.get_xlim() + ymin, ymax = ax.get_ylim() + (xmin, ymin), (xmax, ymax) = ax.transScale.transform( + [(xmin, ymin), (xmax, ymax)]) + + # mouse position in scaled (e.g., log) data coordinates + x, y = ax.transScale.transform((event.xdata, event.ydata)) + + scale_factor = 0.85 ** event.step + new_xmin = x - (x - xmin) * scale_factor + new_xmax = x + (xmax - x) * scale_factor + new_ymin = y - (y - ymin) * scale_factor + new_ymax = y + (ymax - y) * scale_factor + + inv_scale = ax.transScale.inverted() + (new_xmin, new_ymin), (new_xmax, new_ymax) = inv_scale.transform( + [(new_xmin, new_ymin), (new_xmax, new_ymax)]) + + ax.set_xlim(new_xmin, new_xmax) + ax.set_ylim(new_ymin, new_ymax) + + ax.figure.canvas.draw_idle() + + class NonGuiException(Exception): """Raised when trying show a figure in a non-GUI backend.""" pass @@ -2653,11 +2698,14 @@ def __init__(self, canvas, num): self.key_press_handler_id = None self.button_press_handler_id = None + self.scroll_handler_id = None if rcParams['toolbar'] != 'toolmanager': self.key_press_handler_id = self.canvas.mpl_connect( 'key_press_event', key_press_handler) self.button_press_handler_id = self.canvas.mpl_connect( 'button_press_event', button_press_handler) + self.scroll_handler_id = self.canvas.mpl_connect( + 'scroll_event', scroll_handler) self.toolmanager = (ToolManager(canvas.figure) if mpl.rcParams['toolbar'] == 'toolmanager' diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index c65d39415472..7a2b28262249 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -407,6 +407,11 @@ def button_press_handler( canvas: FigureCanvasBase | None = ..., toolbar: NavigationToolbar2 | None = ..., ) -> None: ... +def scroll_handler( + event: MouseEvent, + canvas: FigureCanvasBase | None = ..., + toolbar: NavigationToolbar2 | None = ..., +) -> None: ... class NonGuiException(Exception): ... @@ -415,6 +420,7 @@ class FigureManagerBase: num: int | str key_press_handler_id: int | None button_press_handler_id: int | None + scroll_handler_id: int | None toolmanager: ToolManager | None toolbar: NavigationToolbar2 | ToolContainerBase | None def __init__(self, canvas: FigureCanvasBase, num: int | str) -> None: ... From b98840be02ed00f397cd73fa3b03303b5ab06d23 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Mon, 19 May 2025 14:46:20 +0200 Subject: [PATCH 092/110] Prepare for MetaFont/PK font support. TeX MetaFont (.mf) fonts (e.g., from `\usepackage{concmath}`) need to be converted to raster (.pk) fonts before usage. In dvipng or pdftex this is done by invoking mktexpk via kpsewhich. This patch ensures that our calls to kpsewhich likewise also generate and finds the raster files (in Matplotlib's own temporary cache) when needed. The resolution (600dpi) is the documented default (see e.g. the -D option in `man kpsewhich`). Note that this is only a preliminary step towards full MF/PK font support, which would also require parsing the PK file and embedding the glyphs into our final output. --- lib/matplotlib/dviread.py | 57 +++++++++++++++++++-------- lib/matplotlib/mpl-data/kpsewhich.lua | 3 +- lib/matplotlib/tests/test_dviread.py | 42 +++++++++++++++++++- 3 files changed, 84 insertions(+), 18 deletions(-) diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 1a0f9219a498..702543f9db26 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -32,6 +32,7 @@ import fontTools.agl import numpy as np +import matplotlib as mpl from matplotlib import _api, cbook, font_manager from matplotlib.ft2font import LoadFlags @@ -127,7 +128,13 @@ def glyph_name_or_index(self): def _as_unicode_or_name(self): if self.font.subfont: raise NotImplementedError("Indexing TTC fonts is not supported yet") - face = font_manager.get_font(self.font.resolve_path()) + path = self.font.resolve_path() + if path.name.lower().endswith("pk"): + # PK fonts have no encoding information; report glyphs as ASCII but + # with a "?" to indicate that this is just a guess. + return (f"{chr(self.glyph)}?" if chr(self.glyph).isprintable() else + f"pk{self.glyph:#02x}") + face = font_manager.get_font(path) glyph_name = face.get_glyph_name(self.index) glyph_str = fontTools.agl.toUnicode(glyph_name) return glyph_str or glyph_name @@ -758,13 +765,24 @@ def _height_depth_of(self, char): def resolve_path(self): if self._path is None: - psfont = PsfontsMap(find_tex_file("pdftex.map"))[self.texname] - if psfont.filename is None: - raise ValueError("No usable font file found for {} ({}); " - "the font may lack a Type-1 version" - .format(psfont.psname.decode("ascii"), - psfont.texname.decode("ascii"))) - self._path = Path(psfont.filename) + fontmap = PsfontsMap(find_tex_file("pdftex.map")) + try: + psfont = fontmap[self.texname] + except LookupError as exc: + try: + find_tex_file(f"{self.texname.decode('ascii')}.mf") + except FileNotFoundError: + raise exc from None + else: + self._path = Path(find_tex_file( + f"{self.texname.decode('ascii')}.600pk")) + else: + if psfont.filename is None: + raise ValueError("No usable font file found for {} ({}); " + "the font may lack a Type-1 version" + .format(psfont.psname.decode("ascii"), + psfont.texname.decode("ascii"))) + self._path = Path(psfont.filename) return self._path @cached_property @@ -773,6 +791,8 @@ def subfont(self): @cached_property def effects(self): + if self.resolve_path().match("*.600pk"): + return {} return PsfontsMap(find_tex_file("pdftex.map"))[self.texname].effects def _index_dvi_to_freetype(self, idx): @@ -1233,9 +1253,12 @@ def __new__(cls): def _new_proc(self): return subprocess.Popen( - ["luatex", "--luaonly", - str(cbook._get_data_path("kpsewhich.lua"))], - stdin=subprocess.PIPE, stdout=subprocess.PIPE) + ["luatex", "--luaonly", str(cbook._get_data_path("kpsewhich.lua"))], + # mktexpk logs to stderr; suppress that. + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + # Store generated pk fonts in our own cache. + env={"MT_VARTEXFONTS": str(Path(mpl.get_cachedir(), "vartexfonts")), + **os.environ}) def search(self, filename): if self._proc.poll() is not None: # Dead, restart it. @@ -1287,13 +1310,16 @@ def find_tex_file(filename): kwargs = {'env': {**os.environ, 'command_line_encoding': 'utf-8'}, 'encoding': 'utf-8'} else: # On POSIX, run through the equivalent of os.fsdecode(). - kwargs = {'encoding': sys.getfilesystemencoding(), + kwargs = {'env': {**os.environ}, + 'encoding': sys.getfilesystemencoding(), 'errors': 'surrogateescape'} + kwargs['env'].update( + MT_VARTEXFONTS=str(Path(mpl.get_cachedir(), "vartexfonts"))) try: - path = (cbook._check_and_log_subprocess(['kpsewhich', filename], - _log, **kwargs) - .rstrip('\n')) + path = cbook._check_and_log_subprocess( + ['kpsewhich', '-mktex=pk', filename], _log, **kwargs, + ).rstrip('\n') except (FileNotFoundError, RuntimeError): path = None @@ -1327,7 +1353,6 @@ def _print_fields(*args): print(" ".join(map("{:>11}".format, args))) with Dvi(args.filename, args.dpi) as dvi: - fontmap = PsfontsMap(find_tex_file('pdftex.map')) for page in dvi: print(f"=== NEW PAGE === " f"(w: {page.width}, h: {page.height}, d: {page.descent})") diff --git a/lib/matplotlib/mpl-data/kpsewhich.lua b/lib/matplotlib/mpl-data/kpsewhich.lua index 8e9172a45082..dc526effeebe 100644 --- a/lib/matplotlib/mpl-data/kpsewhich.lua +++ b/lib/matplotlib/mpl-data/kpsewhich.lua @@ -1,3 +1,4 @@ -- see dviread._LuatexKpsewhich kpse.set_program_name("latex") -while true do print(kpse.lookup(io.read():gsub("\r", ""))); io.flush(); end +kpse.init_prog("", 600, "ljfour") +while true do print(kpse.lookup(io.read():gsub("\r", ""), {mktexpk=true})); io.flush(); end diff --git a/lib/matplotlib/tests/test_dviread.py b/lib/matplotlib/tests/test_dviread.py index 7aecd1fc03ae..33fe9bb150d2 100644 --- a/lib/matplotlib/tests/test_dviread.py +++ b/lib/matplotlib/tests/test_dviread.py @@ -3,7 +3,7 @@ import shutil from matplotlib import cbook, dviread as dr -from matplotlib.testing import subprocess_run_for_testing +from matplotlib.testing import subprocess_run_for_testing, _has_tex_package import pytest @@ -105,3 +105,43 @@ def test_dviread(tmp_path, engine, monkeypatch): ] correct = json.loads((dirpath / f"{engine}.json").read_text()) assert data == correct + + +@pytest.mark.skipif(shutil.which("latex") is None, reason="latex is not available") +@pytest.mark.skipif(not _has_tex_package("concmath"), reason="needs concmath.sty") +def test_dviread_pk(tmp_path): + (tmp_path / "test.tex").write_text(r""" + \documentclass{article} + \usepackage{concmath} + \pagestyle{empty} + \begin{document} + Hi! + \end{document} + """) + subprocess_run_for_testing( + ["latex", "test.tex"], cwd=tmp_path, check=True, capture_output=True) + with dr.Dvi(tmp_path / "test.dvi", None) as dvi: + pages = [*dvi] + data = [ + { + "text": [ + [ + t.x, t.y, + t._as_unicode_or_name(), + t.font.resolve_path().name, + round(t.font.size, 2), + t.font.effects, + ] for t in page.text + ], + "boxes": [[b.x, b.y, b.height, b.width] for b in page.boxes] + } for page in pages + ] + correct = [{ + 'boxes': [], + 'text': [ + [5046272, 4128768, 'H?', 'ccr10.600pk', 9.96, {}], + [5530510, 4128768, 'i?', 'ccr10.600pk', 9.96, {}], + [5716195, 4128768, '!?', 'ccr10.600pk', 9.96, {}], + ], + }] + assert data == correct From ccf199496c4089ab17d5d0aebcad8e2534836dbb Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 11 Sep 2025 11:10:20 +0200 Subject: [PATCH 093/110] Better document super call --- src/_macosx.m | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_macosx.m b/src/_macosx.m index 1dbd52bb0a14..70babd39c09c 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -572,7 +572,7 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) }, }; -static PyTypeObject FigureManagerType; +static PyTypeObject FigureManagerType; // forward declaration, needed in destroy() typedef struct { PyObject_HEAD @@ -689,7 +689,8 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) [self->window close]; self->window = NULL; - // call super().destroy() + // call super(self, FigureManager).destroy() - it seems we need the + // explicit arguments, and just super() doesn't work in the C API. PyObject *super_obj = PyObject_CallFunctionObjArgs( (PyObject *)&PySuper_Type, (PyObject *)&FigureManagerType, From 0e8ccea15fe9f6dd6b5e2605bb732ec89b52888c Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Wed, 22 Jan 2025 09:54:10 -0500 Subject: [PATCH 094/110] CI: remove xfail on OSX + tk due to issues in image --- lib/matplotlib/tests/test_backends_interactive.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 9725a79397bc..57d0c4ef5675 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -107,13 +107,7 @@ def _get_available_interactive_backends(): elif env["MPLBACKEND"].startswith('wx') and sys.platform == 'darwin': # ignore on macosx because that's currently broken (github #16849) marks.append(pytest.mark.xfail(reason='github #16849')) - elif (env['MPLBACKEND'] == 'tkagg' and - ('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and - sys.platform == 'darwin' and - sys.version_info[:2] < (3, 11) - ): - marks.append( # https://github.com/actions/setup-python/issues/649 - pytest.mark.xfail(reason='Tk version mismatch on Azure macOS CI')) + envs.append(({**env, 'BACKEND_DEPS': ','.join(deps)}, marks)) return envs From 623fe4d8d4818d2cbfca3c5e2cdf961f31a133ca Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 11 Sep 2025 22:34:48 +0200 Subject: [PATCH 095/110] Fix typos --- doc/release/next_whats_new/pyplot-register-figure.rst | 4 ++-- lib/matplotlib/tests/test_backends_interactive.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/release/next_whats_new/pyplot-register-figure.rst b/doc/release/next_whats_new/pyplot-register-figure.rst index c619aa428695..86ffcbf2294a 100644 --- a/doc/release/next_whats_new/pyplot-register-figure.rst +++ b/doc/release/next_whats_new/pyplot-register-figure.rst @@ -50,8 +50,8 @@ reset to `.FigureCanvasBase` when the figure is closed. `.Figure.savefig` uses the current canvas to save the figure (if possible). Since `.FigureCanvasBase` can not render the figure, when saving the figure, it will fallback to a suitable canvas subclass, e.g. `.FigureCanvasAgg` for raster outputs such as png. -Any Agg-based backend will create the same file output, however -There may be slight differences for non-Agg backends; e.g. if you use "GTK4Cairo" as +Any Agg-based backend will create the same file output. However, there may be +slight differences for non-Agg backends; e.g. if you use "GTK4Cairo" as interactive backend, ``fig.savefig("file.png")`` may create a slightly different image depending on whether the figure is registered with pyplot or not. In general, you should not store a reference to the canvas, but rather always diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 18731ab3fa0b..e0763096c630 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -227,7 +227,7 @@ def check_alt_backend(alt_backend): # Ensure that the window is really closed. plt.pause(0.5) - # When the figure is closed, it's manager is removed and the canvas is reset to + # When the figure is closed, its manager is removed and the canvas is reset to # FigureCanvasBase. Saving should still be possible. assert type(fig.canvas) == FigureCanvasBase, str(fig.canvas) result_after = io.BytesIO() From 4dcf15212467336482aa696cfc019d2ef73a6ec5 Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Sun, 14 Sep 2025 22:36:20 +0200 Subject: [PATCH 096/110] Fix stubtest with mypy 18 Closes #30551 by disabling checks for disjoint bases. There may be other solutions like decorating Container and dviread.Text with @disjoint_base, however I've not yet fully understood whether that'd be technically correct. We have survived without disjoint bases checks so far, so at least a a quick fix this should good enough. --- tools/stubtest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/stubtest.py b/tools/stubtest.py index b79ab2f40dd0..d73d966de19e 100644 --- a/tools/stubtest.py +++ b/tools/stubtest.py @@ -108,6 +108,7 @@ def visit_ClassDef(self, node): [ "stubtest", "--mypy-config-file=pyproject.toml", + "--ignore-disjoint-bases", "--allowlist=ci/mypy-stubtest-allowlist.txt", f"--allowlist={p}", "matplotlib", From 22c3bc266cd33f6eb593d50ad0c90092d6a1f897 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 19:02:30 +0000 Subject: [PATCH 097/110] Bump github/codeql-action from 3.30.1 to 3.30.3 in the actions group Bumps the actions group with 1 update: [github/codeql-action](https://github.com/github/codeql-action). Updates `github/codeql-action` from 3.30.1 to 3.30.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/f1f6e5f6af878fb37288ce1c627459e94dbf7d01...192325c86100d080feab897ff886c34abd4c83a3) --- updated-dependencies: - dependency-name: github/codeql-action dependency-version: 3.30.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: actions ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 07c9c1701cf1..e5c31400d72d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -32,7 +32,7 @@ jobs: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 + uses: github/codeql-action/init@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 with: languages: ${{ matrix.language }} @@ -43,4 +43,4 @@ jobs: pip install --user -v . - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@f1f6e5f6af878fb37288ce1c627459e94dbf7d01 # v3.30.1 + uses: github/codeql-action/analyze@192325c86100d080feab897ff886c34abd4c83a3 # v3.30.3 From f05ecf978533c2a95ec74d0bcd42ecc8a9f056c6 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 23 Jun 2025 20:13:36 -0400 Subject: [PATCH 098/110] Make path-to-string conversion a little safer ... by replacing double pointers by fixed-size `std::array`, or a return `tuple`. With gcc (and optimization enabled?), this has no effect on code size, but gives compile-time (and better runtime) checks that there are no out-of-bounds access. --- src/_path.h | 52 +++++++++++++++++++++---------------------- src/_path_wrapper.cpp | 7 +----- 2 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/_path.h b/src/_path.h index c03703776760..6f315f2109e1 100644 --- a/src/_path.h +++ b/src/_path.h @@ -3,12 +3,12 @@ #ifndef MPL_PATH_H #define MPL_PATH_H -#include -#include -#include -#include #include +#include +#include +#include #include +#include #include "agg_conv_contour.h" #include "agg_conv_curve.h" @@ -1051,15 +1051,14 @@ void cleanup_path(PathIterator &path, void quad2cubic(double x0, double y0, double x1, double y1, double x2, double y2, - double *outx, double *outy) + std::array &outx, std::array &outy) { - - outx[0] = x0 + 2./3. * (x1 - x0); - outy[0] = y0 + 2./3. * (y1 - y0); - outx[1] = outx[0] + 1./3. * (x2 - x0); - outy[1] = outy[0] + 1./3. * (y2 - y0); - outx[2] = x2; - outy[2] = y2; + std::get<0>(outx) = x0 + 2./3. * (x1 - x0); + std::get<0>(outy) = y0 + 2./3. * (y1 - y0); + std::get<1>(outx) = std::get<0>(outx) + 1./3. * (x2 - x0); + std::get<1>(outy) = std::get<0>(outy) + 1./3. * (y2 - y0); + std::get<2>(outx) = x2; + std::get<2>(outy) = y2; } @@ -1104,27 +1103,27 @@ void __add_number(double val, char format_code, int precision, template bool __convert_to_string(PathIterator &path, int precision, - char **codes, + const std::array &codes, bool postfix, std::string& buffer) { const char format_code = 'f'; - double x[3]; - double y[3]; + std::array x; + std::array y; double last_x = 0.0; double last_y = 0.0; unsigned code; - while ((code = path.vertex(&x[0], &y[0])) != agg::path_cmd_stop) { + while ((code = path.vertex(&std::get<0>(x), &std::get<0>(y))) != agg::path_cmd_stop) { if (code == CLOSEPOLY) { - buffer += codes[4]; + buffer += std::get<4>(codes); } else if (code < 5) { size_t size = NUM_VERTICES[code]; for (size_t i = 1; i < size; ++i) { - unsigned subcode = path.vertex(&x[i], &y[i]); + unsigned subcode = path.vertex(&x.at(i), &y.at(i)); if (subcode != code) { return false; } @@ -1133,29 +1132,29 @@ bool __convert_to_string(PathIterator &path, /* For formats that don't support quad curves, convert to cubic curves */ if (code == CURVE3 && codes[code - 1][0] == '\0') { - quad2cubic(last_x, last_y, x[0], y[0], x[1], y[1], x, y); + quad2cubic(last_x, last_y, x.at(0), y.at(0), x.at(1), y.at(1), x, y); code++; size = 3; } if (!postfix) { - buffer += codes[code - 1]; + buffer += codes.at(code - 1); buffer += ' '; } for (size_t i = 0; i < size; ++i) { - __add_number(x[i], format_code, precision, buffer); + __add_number(x.at(i), format_code, precision, buffer); buffer += ' '; - __add_number(y[i], format_code, precision, buffer); + __add_number(y.at(i), format_code, precision, buffer); buffer += ' '; } if (postfix) { - buffer += codes[code - 1]; + buffer += codes.at(code - 1); } - last_x = x[size - 1]; - last_y = y[size - 1]; + last_x = x.at(size - 1); + last_y = y.at(size - 1); } else { // Unknown code value return false; @@ -1174,7 +1173,7 @@ bool convert_to_string(PathIterator &path, bool simplify, SketchParams sketch_params, int precision, - char **codes, + const std::array &codes, bool postfix, std::string& buffer) { @@ -1211,7 +1210,6 @@ bool convert_to_string(PathIterator &path, sketch_t sketch(curve, sketch_params.scale, sketch_params.length, sketch_params.randomness); return __convert_to_string(sketch, precision, codes, postfix, buffer); } - } template diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index 2a297e49ac92..6375351cb52e 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -252,16 +252,11 @@ static py::object Py_convert_to_string(mpl::PathIterator path, agg::trans_affine trans, agg::rect_d cliprect, std::optional simplify, SketchParams sketch, int precision, - std::array codes_obj, bool postfix) + const std::array &codes, bool postfix) { - char *codes[5]; std::string buffer; bool status; - for (auto i = 0; i < 5; ++i) { - codes[i] = const_cast(codes_obj[i].c_str()); - } - if (!simplify.has_value()) { simplify = path.should_simplify(); } From 3373c9696a29cdb3a2fd1b11ae0c847329a098f6 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 9 Sep 2025 03:58:15 -0400 Subject: [PATCH 099/110] MNT: Fix type of _finalize_polygon::closed_only It is `bool` for the Python wrapper, while internally `int`, but can be `bool` consistently. Also mark it as `inline` since it's used in a template and the compiler warns about a possible ODR violation (which isn't a problem since it's only used in one file.) --- src/_path.h | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/_path.h b/src/_path.h index 6f315f2109e1..dc9058535f5f 100644 --- a/src/_path.h +++ b/src/_path.h @@ -43,7 +43,8 @@ struct XY typedef std::vector Polygon; -void _finalize_polygon(std::vector &result, int closed_only) +inline void +_finalize_polygon(std::vector &result, bool closed_only) { if (result.size() == 0) { return; @@ -691,12 +692,12 @@ clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside, std::vecto // Empty polygons aren't very useful, so skip them if (polygon1.size()) { - _finalize_polygon(results, 1); + _finalize_polygon(results, true); results.push_back(polygon1); } } while (code != agg::path_cmd_stop); - _finalize_polygon(results, 1); + _finalize_polygon(results, true); } template @@ -956,7 +957,7 @@ void convert_path_to_polygons(PathIterator &path, agg::trans_affine &trans, double width, double height, - int closed_only, + bool closed_only, std::vector &result) { typedef agg::conv_transform transformed_path_t; @@ -980,7 +981,7 @@ void convert_path_to_polygons(PathIterator &path, while ((code = curve.vertex(&x, &y)) != agg::path_cmd_stop) { if ((code & agg::path_cmd_end_poly) == agg::path_cmd_end_poly) { - _finalize_polygon(result, 1); + _finalize_polygon(result, true); polygon = &result.emplace_back(); } else { if (code == agg::path_cmd_move_to) { From 56b9bffc312d8e0b0df2ec3be3fc6bb601e0759d Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 9 Sep 2025 04:56:54 -0400 Subject: [PATCH 100/110] path: Simplify extent_limits implementation a bit By using the existing `XY` type to replace x/y pairs, and taking advantage of struct methods. --- src/_path.h | 70 ++++++++++++++++++++----------------------- src/_path_wrapper.cpp | 12 ++++---- 2 files changed, 39 insertions(+), 43 deletions(-) diff --git a/src/_path.h b/src/_path.h index dc9058535f5f..de6749e21995 100644 --- a/src/_path.h +++ b/src/_path.h @@ -312,43 +312,39 @@ inline bool point_on_path( struct extent_limits { - double x0; - double y0; - double x1; - double y1; - double xm; - double ym; -}; + XY start; + XY end; + /* minpos is the minimum positive values in the data; used by log scaling. */ + XY minpos; -void reset_limits(extent_limits &e) -{ - e.x0 = std::numeric_limits::infinity(); - e.y0 = std::numeric_limits::infinity(); - e.x1 = -std::numeric_limits::infinity(); - e.y1 = -std::numeric_limits::infinity(); - /* xm and ym are the minimum positive values in the data, used - by log scaling */ - e.xm = std::numeric_limits::infinity(); - e.ym = std::numeric_limits::infinity(); -} + extent_limits() : start{0,0}, end{0,0}, minpos{0,0} { + reset(); + } -inline void update_limits(double x, double y, extent_limits &e) -{ - if (x < e.x0) - e.x0 = x; - if (y < e.y0) - e.y0 = y; - if (x > e.x1) - e.x1 = x; - if (y > e.y1) - e.y1 = y; - /* xm and ym are the minimum positive values in the data, used - by log scaling */ - if (x > 0.0 && x < e.xm) - e.xm = x; - if (y > 0.0 && y < e.ym) - e.ym = y; -} + void reset() + { + start.x = std::numeric_limits::infinity(); + start.y = std::numeric_limits::infinity(); + end.x = -std::numeric_limits::infinity(); + end.y = -std::numeric_limits::infinity(); + minpos.x = std::numeric_limits::infinity(); + minpos.y = std::numeric_limits::infinity(); + } + + void update(double x, double y) + { + start.x = std::min(start.x, x); + start.y = std::min(start.y, y); + end.x = std::max(end.x, x); + end.y = std::max(end.y, y); + if (x > 0.0) { + minpos.x = std::min(minpos.x, x); + } + if (y > 0.0) { + minpos.y = std::min(minpos.y, y); + } + } +}; template void update_path_extents(PathIterator &path, agg::trans_affine &trans, extent_limits &extents) @@ -367,7 +363,7 @@ void update_path_extents(PathIterator &path, agg::trans_affine &trans, extent_li if ((code & agg::path_cmd_end_poly) == agg::path_cmd_end_poly) { continue; } - update_limits(x, y, extents); + extents.update(x, y); } } @@ -390,7 +386,7 @@ void get_path_collection_extents(agg::trans_affine &master_transform, agg::trans_affine trans; - reset_limits(extent); + extent.reset(); for (auto i = 0; i < N; ++i) { typename PathGenerator::path_iterator path(paths(i % Npaths)); diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index 6375351cb52e..fd80b02b85ae 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -68,15 +68,15 @@ Py_get_path_collection_extents(agg::trans_affine master_transform, py::ssize_t dims[] = { 2, 2 }; py::array_t extents(dims); - *extents.mutable_data(0, 0) = e.x0; - *extents.mutable_data(0, 1) = e.y0; - *extents.mutable_data(1, 0) = e.x1; - *extents.mutable_data(1, 1) = e.y1; + *extents.mutable_data(0, 0) = e.start.x; + *extents.mutable_data(0, 1) = e.start.y; + *extents.mutable_data(1, 0) = e.end.x; + *extents.mutable_data(1, 1) = e.end.y; py::ssize_t minposdims[] = { 2 }; py::array_t minpos(minposdims); - *minpos.mutable_data(0) = e.xm; - *minpos.mutable_data(1) = e.ym; + *minpos.mutable_data(0) = e.minpos.x; + *minpos.mutable_data(1) = e.minpos.y; return py::make_tuple(extents, minpos); } From f2a9dac0b4b5783ea6053889847299eacd1b924a Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Tue, 9 Sep 2025 05:15:31 -0400 Subject: [PATCH 101/110] path: Simplify parts of clip_path_to_rect Use `XY` type to shorten internals, and `agg::rect_d::normalize` to shorten initialization. --- src/_path.h | 93 ++++++++++++++++++++----------------------- src/_path_wrapper.cpp | 4 +- 2 files changed, 44 insertions(+), 53 deletions(-) diff --git a/src/_path.h b/src/_path.h index de6749e21995..226d60231682 100644 --- a/src/_path.h +++ b/src/_path.h @@ -26,6 +26,8 @@ struct XY double x; double y; + XY() : x(0), y(0) {} + XY(double x_, double y_) : x(x_), y(y_) { } @@ -521,12 +523,14 @@ struct bisectx { } - inline void bisect(double sx, double sy, double px, double py, double *bx, double *by) const + inline XY bisect(const XY s, const XY p) const { - *bx = m_x; - double dx = px - sx; - double dy = py - sy; - *by = sy + dy * ((m_x - sx) / dx); + double dx = p.x - s.x; + double dy = p.y - s.y; + return { + m_x, + s.y + dy * ((m_x - s.x) / dx), + }; } }; @@ -536,9 +540,9 @@ struct xlt : public bisectx { } - inline bool is_inside(double x, double y) const + inline bool is_inside(const XY point) const { - return x <= m_x; + return point.x <= m_x; } }; @@ -548,9 +552,9 @@ struct xgt : public bisectx { } - inline bool is_inside(double x, double y) const + inline bool is_inside(const XY point) const { - return x >= m_x; + return point.x >= m_x; } }; @@ -562,12 +566,14 @@ struct bisecty { } - inline void bisect(double sx, double sy, double px, double py, double *bx, double *by) const + inline XY bisect(const XY s, const XY p) const { - *by = m_y; - double dx = px - sx; - double dy = py - sy; - *bx = sx + dx * ((m_y - sy) / dy); + double dx = p.x - s.x; + double dy = p.y - s.y; + return { + s.x + dx * ((m_y - s.y) / dy), + m_y, + }; } }; @@ -577,9 +583,9 @@ struct ylt : public bisecty { } - inline bool is_inside(double x, double y) const + inline bool is_inside(const XY point) const { - return y <= m_y; + return point.y <= m_y; } }; @@ -589,9 +595,9 @@ struct ygt : public bisecty { } - inline bool is_inside(double x, double y) const + inline bool is_inside(const XY point) const { - return y >= m_y; + return point.y >= m_y; } }; } @@ -606,46 +612,30 @@ inline void clip_to_rect_one_step(const Polygon &polygon, Polygon &result, const return; } - auto [sx, sy] = polygon.back(); - for (auto [px, py] : polygon) { - sinside = filter.is_inside(sx, sy); - pinside = filter.is_inside(px, py); + auto s = polygon.back(); + for (auto p : polygon) { + sinside = filter.is_inside(s); + pinside = filter.is_inside(p); if (sinside ^ pinside) { - double bx, by; - filter.bisect(sx, sy, px, py, &bx, &by); - result.emplace_back(bx, by); + result.emplace_back(filter.bisect(s, p)); } if (pinside) { - result.emplace_back(px, py); + result.emplace_back(p); } - sx = px; - sy = py; + s = p; } } template -void -clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside, std::vector &results) +auto +clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside) { - double xmin, ymin, xmax, ymax; - if (rect.x1 < rect.x2) { - xmin = rect.x1; - xmax = rect.x2; - } else { - xmin = rect.x2; - xmax = rect.x1; - } - - if (rect.y1 < rect.y2) { - ymin = rect.y1; - ymax = rect.y2; - } else { - ymin = rect.y2; - ymax = rect.y1; - } + rect.normalize(); + auto xmin = rect.x1, xmax = rect.x2; + auto ymin = rect.y1, ymax = rect.y2; if (!inside) { std::swap(xmin, xmax); @@ -656,26 +646,27 @@ clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside, std::vecto curve_t curve(path); Polygon polygon1, polygon2; - double x = 0, y = 0; + XY point; unsigned code = 0; curve.rewind(0); + std::vector results; do { // Grab the next subpath and store it in polygon1 polygon1.clear(); do { if (code == agg::path_cmd_move_to) { - polygon1.emplace_back(x, y); + polygon1.emplace_back(point); } - code = curve.vertex(&x, &y); + code = curve.vertex(&point.x, &point.y); if (code == agg::path_cmd_stop) { break; } if (code != agg::path_cmd_move_to) { - polygon1.emplace_back(x, y); + polygon1.emplace_back(point); } } while ((code & agg::path_cmd_end_poly) != agg::path_cmd_end_poly); @@ -694,6 +685,8 @@ clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside, std::vecto } while (code != agg::path_cmd_stop); _finalize_polygon(results, true); + + return results; } template diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index fd80b02b85ae..802189c428d3 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -109,9 +109,7 @@ Py_path_in_path(mpl::PathIterator a, agg::trans_affine atrans, static py::list Py_clip_path_to_rect(mpl::PathIterator path, agg::rect_d rect, bool inside) { - std::vector result; - - clip_path_to_rect(path, rect, inside, result); + auto result = clip_path_to_rect(path, rect, inside); return convert_polygon_vector(result); } From 31f6806e7b819c552fec9c460f67a9ac66f05211 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 16 Sep 2025 13:46:18 -0400 Subject: [PATCH 102/110] CI: remove macos13 The VM is being dropped by GH [1] on macOS is no longer supported by homebrew or Apple. This job is starting to fail on installing homebrew dependencies. Rather than try to fix this, move on to the next image which we need to do by the end of the CY anyway. This means we no longer are testing on intel macs. [1] https://github.blog/changelog/2025-07-11-upcoming-changes-to-macos-hosted-runners-macos-latest-migration-and-xcode-support-policy-updates/#macos-13-is-closing-down --- .github/workflows/tests.yml | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c6f1e25c1e92..13b09cc4d6ee 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -77,8 +77,7 @@ jobs: pygobject-ver: '<3.52.0' - os: ubuntu-24.04 python-version: '3.12' - - os: macos-13 # This runner is on Intel chips. - # merge numpy and pandas install in nighties test when this runner is dropped + - os: macos-14 # This runner is on M1 (arm64) chips. python-version: '3.11' - os: macos-14 # This runner is on M1 (arm64) chips. python-version: '3.12' @@ -297,13 +296,7 @@ jobs: python -m pip install pytz tzdata # Must be installed for Pandas. python -m pip install \ --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \ - --upgrade --only-binary=:all: numpy - # wheels for intel osx is not always available on nightly wheels index, merge this back into - # the above install command when the OSX-13 (intel) runners are dropped. - python -m pip install \ - --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \ - --upgrade --only-binary=:all: pandas || true - + --upgrade --only-binary=:all: numpy pandas - name: Install Matplotlib run: | From c85328ae37e78678f639f12d014afa3d1b3500ae Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Mon, 15 Sep 2025 22:16:03 +0200 Subject: [PATCH 103/110] DOC: improve description of boilerplate.py Closes #27190. --- lib/matplotlib/pyplot.py | 8 ++++++-- tools/boilerplate.py | 19 +++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 4531cbd3b261..aaa04d464f30 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -1,5 +1,9 @@ -# Note: The first part of this file can be modified in place, but the latter -# part is autogenerated by the boilerplate.py script. +# Note: The first part of this file is hand-written and must be edited +# in-place. The second part, starting with +# ### REMAINING CONTENT GENERATED BY boilerplate.py ### +# is generated by the script boilerplate.py. It must not be edited here +# because all changes will be overwritten by the next run of the script. +# For more information see the description in boilerplate.py. """ `matplotlib.pyplot` is a state-based interface to matplotlib. It provides diff --git a/tools/boilerplate.py b/tools/boilerplate.py index 11ec15ac1c44..a617d12c7072 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -1,12 +1,19 @@ """ Script to autogenerate pyplot wrappers. -When this script is run, the current contents of pyplot are -split into generatable and non-generatable content (via the magic header -:attr:`PYPLOT_MAGIC_HEADER`) and the generatable content is overwritten. -Hence, the non-generatable content should be edited in the pyplot.py file -itself, whereas the generatable content must be edited via templates in -this file. +pyplot.py consists of two parts: a hand-written part at the top, and an +automatically generated part at the bottom, starting with the comment + + ### REMAINING CONTENT GENERATED BY boilerplate.py ### + +This script generates the automatically generated part of pyplot.py. It +consists of colormap setter functions and wrapper functions for methods +of Figure and Axes. Whenever the API of one of the wrapped methods changes, +this script has to be rerun to keep pyplot.py up to date. + +The test ``lib/matplotlib/test_pyplot.py::test_pyplot_up_to_date`` checks +that the autogenerated part of pyplot.py is up to date. It will fail in the +case of an API mismatch and remind the developer to rerun this script. """ # Although it is possible to dynamically generate the pyplot functions at From b65c17e58b322247e9318b16fdcc9628292b78b9 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 16 Sep 2025 17:03:16 -0400 Subject: [PATCH 104/110] CI: move 3.13 job to macos-15 --- .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 13b09cc4d6ee..aeb84468571f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -83,7 +83,7 @@ jobs: python-version: '3.12' # https://github.com/matplotlib/matplotlib/issues/29732 pygobject-ver: '<3.52.0' - - os: macos-14 # This runner is on M1 (arm64) chips. + - os: macos-15 # This runner is on M1 (arm64) chips. python-version: '3.13' # https://github.com/matplotlib/matplotlib/issues/29732 pygobject-ver: '<3.52.0' From c18a1656eb49de5eee99c55f475022fe8e7f60a2 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 16 Sep 2025 21:33:28 -0400 Subject: [PATCH 105/110] CI: pin back pygobject --- .github/workflows/tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aeb84468571f..6da9c7a0a8fa 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -79,6 +79,8 @@ jobs: python-version: '3.12' - os: macos-14 # This runner is on M1 (arm64) chips. python-version: '3.11' + # https://github.com/matplotlib/matplotlib/issues/29732 + pygobject-ver: '<3.52.0' - os: macos-14 # This runner is on M1 (arm64) chips. python-version: '3.12' # https://github.com/matplotlib/matplotlib/issues/29732 From ec72aa03df308f4695220b509d119ff96dab4724 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Tue, 16 Sep 2025 10:50:57 +0200 Subject: [PATCH 106/110] Copy-edit the "fonts in pdf and postscript" table. Remove unnecessary info snippets e.g. re: fractal rendering (this isn't a table for font designers); merge Type 42 entry together with TrueType (they are essentially the same technology). --- galleries/users_explain/text/fonts.py | 62 ++++++++++----------------- 1 file changed, 22 insertions(+), 40 deletions(-) diff --git a/galleries/users_explain/text/fonts.py b/galleries/users_explain/text/fonts.py index 067ed2f3932a..40cc9eaa93eb 100644 --- a/galleries/users_explain/text/fonts.py +++ b/galleries/users_explain/text/fonts.py @@ -27,28 +27,24 @@ Matplotlib supports three font specifications (in addition to pdf 'core fonts', which are explained later in the guide): -.. table:: Type of Fonts - - +--------------------------+----------------------------+----------------------------+ - | Type 1 (PDF with usetex) | Type 3 (PDF/PS) | TrueType (PDF) | - +==========================+============================+============================+ - | One of the oldest types, | Similar to Type 1 in | Newer than previous types, | - | introduced by Adobe | terms of introduction | used commonly today, | - | | | introduced by Apple | - +--------------------------+----------------------------+----------------------------+ - | Restricted subset of | Full PostScript language, | Includes a virtual machine | - | PostScript, charstrings | allows embedding arbitrary | that can execute code! | - | are in bytecode | code (in theory, even | | - | | render fractals when | | - | | rasterizing!) | | - +--------------------------+----------------------------+----------------------------+ - | Supports font | Does not support font | Supports font hinting | - | hinting | hinting | (virtual machine processes | - | | | the "hints") | - +--------------------------+----------------------------+----------------------------+ - | Subsetted by code in | Subsetted via external module | - | `matplotlib._type1font` | `fontTools `__ | - +--------------------------+----------------------------+----------------------------+ +.. table:: Types of Fonts + + +--------------------------+----------------------------+-------------------------------+ + | Type 1 (PDF with usetex) | Type 3 (PDF/PS) | TrueType (PDF) / Type 42 (PS) | + +==========================+============================+===============================+ + | Old font types introduced by Adobe. | Newer font type introduced by | + | | Apple; commonly used today. | + +--------------------------+----------------------------+-------------------------------+ + | Restricted subset of | Full PostScript language, | Includes a virtual machine | + | PostScript, charstrings | allows embedding arbitrary | that can execute code. | + | are in bytecode. | code. | | + +--------------------------+----------------------------+-------------------------------+ + | Supports font hinting. | Does not support font | Supports font hinting, | + | | hinting. | through the virtual machine. | + +--------------------------+----------------------------+-------------------------------+ + | Subsetted by code in | Subsetted via external module | + | `matplotlib._type1font`. | `fontTools `__. | + +--------------------------+----------------------------+-------------------------------+ .. note:: @@ -59,23 +55,9 @@ __ https://helpx.adobe.com/fonts/kb/postscript-type-1-fonts-end-of-support.html -Other font specifications which Matplotlib supports: - -- Type 42 fonts (PS): - - - PostScript wrapper around TrueType fonts - - 42 is the `Answer to Life, the Universe, and Everything! - `_ - - Matplotlib uses the external library - `fontTools `__ to subset these types of - fonts - -- OpenType fonts: - - - OpenType is a new standard for digital type fonts, developed jointly by - Adobe and Microsoft - - Generally contain a much larger character set! - - Limited support with Matplotlib +Matplotlib also provides limited support for OpenType fonts, a newer standard +developed jointly by Adobe and Microsoft; such fonts generally contain a much +larger character set. Font subsetting ^^^^^^^^^^^^^^^ @@ -201,4 +183,4 @@ A majority of this work was done by Aitik Gupta supported by Google Summer of Code 2021. -""" +""" # noqa: E501 From 554b9c5bf75602c8186b67378b8f66827dd9c8fa Mon Sep 17 00:00:00 2001 From: Tim Hoffmann <2836374+timhoffm@users.noreply.github.com> Date: Thu, 18 Sep 2025 07:20:28 +0200 Subject: [PATCH 107/110] MNT: Move all Colormap extremes setter logic into a single _set_extremes() (#30577) --- lib/matplotlib/colors.py | 43 ++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 679f368bae30..8cacea8fbb05 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -877,9 +877,7 @@ def get_bad(self): def set_bad(self, color='k', alpha=None): """Set the color for masked values.""" - self._rgba_bad = to_rgba(color, alpha) - if self._isinit: - self._set_extremes() + self._set_extremes(bad=(color, alpha)) def get_under(self): """Get the color for low out-of-range values.""" @@ -889,9 +887,7 @@ def get_under(self): def set_under(self, color='k', alpha=None): """Set the color for low out-of-range values.""" - self._rgba_under = to_rgba(color, alpha) - if self._isinit: - self._set_extremes() + self._set_extremes(under=(color, alpha)) def get_over(self): """Get the color for high out-of-range values.""" @@ -901,21 +897,14 @@ def get_over(self): def set_over(self, color='k', alpha=None): """Set the color for high out-of-range values.""" - self._rgba_over = to_rgba(color, alpha) - if self._isinit: - self._set_extremes() + self._set_extremes(over=(color, alpha)) def set_extremes(self, *, bad=None, under=None, over=None): """ Set the colors for masked (*bad*) values and, when ``norm.clip = False``, low (*under*) and high (*over*) out-of-range values. """ - if bad is not None: - self.set_bad(bad) - if under is not None: - self.set_under(under) - if over is not None: - self.set_over(over) + self._set_extremes(bad=bad, under=under, over=over) def with_extremes(self, *, bad=None, under=None, over=None): """ @@ -924,10 +913,26 @@ def with_extremes(self, *, bad=None, under=None, over=None): out-of-range values, have been set accordingly. """ new_cm = self.copy() - new_cm.set_extremes(bad=bad, under=under, over=over) + new_cm._set_extremes(bad=bad, under=under, over=over) return new_cm - def _set_extremes(self): + def _set_extremes(self, bad=None, under=None, over=None): + """ + Set the colors for masked (*bad*) and out-of-range (*under* and *over*) values. + + Parameters that are None are left unchanged. + """ + if bad is not None: + self._rgba_bad = to_rgba(bad) + if under is not None: + self._rgba_under = to_rgba(under) + if over is not None: + self._rgba_over = to_rgba(over) + if self._isinit: + self._update_lut_extremes() + + def _update_lut_extremes(self): + """Ensure than an existing lookup table has the correct extreme values.""" if self._rgba_under: self._lut[self._i_under] = self._rgba_under else: @@ -1154,7 +1159,7 @@ def _init(self): self._lut[:-3, 3] = _create_lookup_table( self.N, self._segmentdata['alpha'], 1) self._isinit = True - self._set_extremes() + self._update_lut_extremes() def set_gamma(self, gamma): """Set a new gamma value and regenerate colormap.""" @@ -1346,7 +1351,7 @@ def _init(self): self._lut = np.zeros((self.N + 3, 4), float) self._lut[:-3] = to_rgba_array(self.colors) self._isinit = True - self._set_extremes() + self._update_lut_extremes() @property def monochrome(self): From 4a20d0fbe2c8af0478246ae716d506e12a6112a0 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 18 Sep 2025 02:02:04 -0400 Subject: [PATCH 108/110] TST: Force Agg backend in test_openin_any_paranoid On some CI, the Qt backend seems to be picked, but something is confused and it fails due to an invalid `DISPLAY` variable. But this test doesn't need an interactive backend, so don't use one. --- lib/matplotlib/tests/test_texmanager.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/tests/test_texmanager.py b/lib/matplotlib/tests/test_texmanager.py index 64dcbf46456d..7e7b5632c7ac 100644 --- a/lib/matplotlib/tests/test_texmanager.py +++ b/lib/matplotlib/tests/test_texmanager.py @@ -70,6 +70,7 @@ def test_openin_any_paranoid(): 'import matplotlib.pyplot as plt;' 'plt.rcParams.update({"text.usetex": True});' 'plt.title("paranoid");' - 'plt.show(block=False);'], - env={**os.environ, 'openin_any': 'p'}, check=True, capture_output=True) + 'plt.gcf().canvas.draw();'], + env={**os.environ, 'MPLBACKEND': 'Agg', 'openin_any': 'p'}, + check=True, capture_output=True) assert completed.stderr == "" From 4576add820d01e1164c37ae31af8fae28b396e46 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 18 Sep 2025 17:11:05 -0400 Subject: [PATCH 109/110] ci: Bump Ubuntu ARM builder to 24.04 Since earlier today, the `ubuntu-22.04-arm` image has actually been Ubuntu 24.04. Instead of working out how to fix that, switch to the newer image explicitly. --- .github/workflows/tests.yml | 12 +++++------- lib/matplotlib/tests/test_axes.py | 5 +++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6da9c7a0a8fa..8ed83a65579b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -60,10 +60,6 @@ jobs: extra-requirements: '-r requirements/testing/extra.txt' # https://github.com/matplotlib/matplotlib/issues/29844 pygobject-ver: '<3.52.0' - - os: ubuntu-22.04-arm - python-version: '3.12' - # https://github.com/matplotlib/matplotlib/issues/29844 - pygobject-ver: '<3.52.0' - name-suffix: "(Extra TeX packages)" os: ubuntu-22.04 python-version: '3.13' @@ -77,6 +73,8 @@ jobs: pygobject-ver: '<3.52.0' - os: ubuntu-24.04 python-version: '3.12' + - os: ubuntu-24.04-arm + python-version: '3.12' - os: macos-14 # This runner is on M1 (arm64) chips. python-version: '3.11' # https://github.com/matplotlib/matplotlib/issues/29732 @@ -150,7 +148,7 @@ jobs: if [[ "${{ matrix.name-suffix }}" != '(Minimum Versions)' ]]; then sudo apt-get install -yy --no-install-recommends ffmpeg poppler-utils fi - if [[ "${{ matrix.os }}" = ubuntu-22.04 || "${{ matrix.os }}" = ubuntu-22.04-arm ]]; then + if [[ "${{ matrix.os }}" = ubuntu-22.04 ]]; then sudo apt-get install -yy --no-install-recommends \ gir1.2-gtk-4.0 \ libgirepository1.0-dev @@ -257,7 +255,7 @@ jobs: ) # PyQt5 does not have any wheels for ARM on Linux. - if [[ "${{ matrix.os }}" != 'ubuntu-22.04-arm' ]]; then + if [[ "${{ matrix.os }}" != 'ubuntu-24.04-arm' ]]; then python -mpip install --upgrade --only-binary :all: pyqt5 && python -c 'import PyQt5.QtCore' && echo 'PyQt5 is available' || @@ -372,7 +370,7 @@ jobs: run: | if [[ "${{ runner.os }}" != 'macOS' ]]; then LCOV_IGNORE_ERRORS=',' # do not ignore any lcov errors by default - if [[ "${{ matrix.os }}" = ubuntu-24.04 ]]; then + if [[ "${{ matrix.os }}" = ubuntu-24.04 || "${{ matrix.os }}" = ubuntu-24.04-arm ]]; then # filter mismatch and unused-entity errors detected by lcov 2.x LCOV_IGNORE_ERRORS='mismatch,unused' fi diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 6679f0a0055b..b25b58c4a727 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1382,7 +1382,8 @@ def test_pcolorargs_5205(): plt.pcolor(X, Y, list(Z[:-1, :-1])) -@image_comparison(['pcolormesh'], remove_text=True) +@image_comparison(['pcolormesh'], remove_text=True, + tol=0.11 if platform.machine() == 'aarch64' else 0) def test_pcolormesh(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -1434,7 +1435,7 @@ def test_pcolormesh_small(): @image_comparison(['pcolormesh_alpha'], extensions=["png", "pdf"], remove_text=True, - tol=0.2 if platform.machine() == "aarch64" else 0) + tol=0.4 if platform.machine() == "aarch64" else 0) def test_pcolormesh_alpha(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False From b2364882c3776a7531e00b50c0e8957219fce32b Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Wed, 17 Sep 2025 22:39:24 -0400 Subject: [PATCH 110/110] MNT: Ignore differing stub for GlyphIndexType This may be an upstream bug [1], but until that is determined, ignore the error to get CI working. [1] https://github.com/python/mypy/issues/19877 --- ci/mypy-stubtest-allowlist.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 46ec06e0a9f1..c8e8e60bc3c9 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -49,3 +49,7 @@ matplotlib\.figure\.FigureBase\.get_figure # getitem method only exists for 3.10 deprecation backcompatability matplotlib\.inset\.InsetIndicator\.__getitem__ + +# Avoid a regression in NewType handling for stubtest +# https://github.com/python/mypy/issues/19877 +matplotlib\.ft2font\.GlyphIndexType\.__init__