diff --git a/.appveyor.yml b/.appveyor.yml index 63746ab2b372..da2aec993979 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -17,7 +17,7 @@ skip_commits: clone_depth: 50 -image: Visual Studio 2017 +image: Visual Studio 2019 environment: @@ -45,8 +45,9 @@ cache: init: - ps: + # Pinned due to https://github.com/mamba-org/mamba/issues/3467 Invoke-Webrequest - -URI https://micro.mamba.pm/api/micromamba/win-64/latest + -URI https://github.com/mamba-org/micromamba-releases/releases/download/1.5.10-0/micromamba-win-64.tar.bz2 -OutFile C:\projects\micromamba.tar.bz2 - ps: C:\PROGRA~1\7-Zip\7z.exe x C:\projects\micromamba.tar.bz2 -aoa -oC:\projects\ - ps: C:\PROGRA~1\7-Zip\7z.exe x C:\projects\micromamba.tar -ttar -aoa -oC:\projects\ diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index 0db8c53b3a79..748d25a01426 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -143,7 +143,7 @@ jobs: path: dist/ - name: Build wheels for CPython 3.13 - uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 + uses: pypa/cibuildwheel@f1859528322d7b29d4493ee241a167807661dfb4 # v2.21.2 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -163,7 +163,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.12 - uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 + uses: pypa/cibuildwheel@f1859528322d7b29d4493ee241a167807661dfb4 # v2.21.2 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -171,7 +171,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.11 - uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 + uses: pypa/cibuildwheel@f1859528322d7b29d4493ee241a167807661dfb4 # v2.21.2 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -179,7 +179,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.10 - uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 + uses: pypa/cibuildwheel@f1859528322d7b29d4493ee241a167807661dfb4 # v2.21.2 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -187,7 +187,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for PyPy - uses: pypa/cibuildwheel@bd033a44476646b606efccdd5eed92d5ea1d77ad # v2.20.0 + uses: pypa/cibuildwheel@f1859528322d7b29d4493ee241a167807661dfb4 # v2.21.2 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -231,9 +231,9 @@ jobs: run: ls dist - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@310b0a4a3b0b78ef57ecda988ee04b132db73ef8 # v1.4.1 + uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 with: subject-path: dist/matplotlib-* - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@ec4db0b4ddc65acdf4bff5fa45ac92d78b56bdf0 # v1.9.0 + uses: pypa/gh-action-pypi-publish@f7600683efdcb7656dec5b29656edb7bc586e597 # v1.10.3 diff --git a/.github/workflows/cygwin.yml b/.github/workflows/cygwin.yml index b8bb4400f2f3..5dee68597d5c 100644 --- a/.github/workflows/cygwin.yml +++ b/.github/workflows/cygwin.yml @@ -29,7 +29,6 @@ on: # 5:47 UTC on Saturdays - cron: "47 5 * * 6" workflow_dispatch: - workflow: "*" permissions: contents: read diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 54e81f06b166..25e2bb344eda 100644 --- a/.github/workflows/nightlies.yml +++ b/.github/workflows/nightlies.yml @@ -59,7 +59,7 @@ jobs: ls -l dist/ - name: Upload wheels to Anaconda Cloud as nightlies - uses: scientific-python/upload-nightly-action@b67d7fcc0396e1128a474d1ab2b48aa94680f9fc # 0.5.0 + uses: scientific-python/upload-nightly-action@82396a2ed4269ba06c6b2988bb4fd568ef3c3d6b # 0.6.1 with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4de46a1ed80f..cd8e43fc6661 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,7 +21,6 @@ on: # 5:47 UTC on Saturdays - cron: "47 5 * * 6" workflow_dispatch: - workflow: "*" env: NO_AT_BRIDGE: 1 # Necessary for GTK3 interactive test. @@ -124,7 +123,7 @@ jobs: allow-prereleases: true - name: Set up Python ${{ matrix.python-version }} - uses: deadsnakes/action@6c8b9b82fe0b4344f4b98f2775fcc395df45e494 # v3.1.0 + uses: deadsnakes/action@e640ac8743173a67cca4d7d77cd837e514bf98e8 # v3.2.0 if: matrix.python-version == '3.13t' with: python-version: '3.13' @@ -261,7 +260,7 @@ jobs: # Preinstall build requirements to enable no-build-isolation builds. python -m pip install --upgrade $PRE \ 'contourpy>=1.0.1' cycler fonttools kiwisolver importlib_resources \ - numpy packaging pillow 'pyparsing!=3.1.0' python-dateutil setuptools-scm \ + packaging pillow 'pyparsing!=3.1.0' python-dateutil setuptools-scm \ 'meson-python>=0.13.1' 'pybind11>=2.6' \ -r requirements/testing/all.txt \ ${{ matrix.extra-requirements }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3c5b330bae67..6068b8b0df83 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ exclude: | ) repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-added-large-files - id: check-docstring-first @@ -28,7 +28,7 @@ repos: - id: trailing-whitespace exclude_types: [svg] - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.10.1 + rev: v1.11.2 hooks: - id: mypy additional_dependencies: @@ -42,7 +42,7 @@ repos: files: lib/matplotlib # Only run when files in lib/matplotlib are changed. pass_filenames: false - repo: https://github.com/pycqa/flake8 - rev: 7.1.0 + rev: 7.1.1 hooks: - id: flake8 additional_dependencies: @@ -67,7 +67,7 @@ repos: name: isort (python) files: ^galleries/tutorials/|^galleries/examples/|^galleries/plot_types/ - repo: https://github.com/rstcheck/rstcheck - rev: v6.2.0 + rev: v6.2.4 hooks: - id: rstcheck additional_dependencies: @@ -79,7 +79,7 @@ repos: - id: yamllint args: ["--strict", "--config-file=.yamllint.yml"] - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.28.6 + rev: 0.29.3 hooks: # TODO: Re-enable this when https://github.com/microsoft/azure-pipelines-vscode/issues/567 is fixed. # - id: check-azure-pipelines diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 4b6e487a418d..1d08a690d8f2 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -1,51 +1,51 @@ # Non-typed (and private) modules/functions -matplotlib.backends.* -matplotlib.tests.* -matplotlib.pylab.* -matplotlib._.* -matplotlib.rcsetup._listify_validator -matplotlib.rcsetup._validate_linestyle -matplotlib.ft2font.Glyph -matplotlib.testing.jpl_units.* -matplotlib.sphinxext.* +matplotlib\.backends\..* +matplotlib\.tests(\..*)? +matplotlib\.pylab\..* +matplotlib\._.* +matplotlib\.rcsetup\._listify_validator +matplotlib\.rcsetup\._validate_linestyle +matplotlib\.ft2font\.Glyph +matplotlib\.testing\.jpl_units\..* +matplotlib\.sphinxext(\..*)? # set methods have heavy dynamic usage of **kwargs, with differences for subclasses # which results in technically inconsistent signatures, but not actually a problem -matplotlib.*\.set$ +matplotlib\..*\.set$ # Typed inline, inconsistencies largely due to imports -matplotlib.pyplot.* -matplotlib.typing.* +matplotlib\.pyplot\..* +matplotlib\.typing\..* # Other decorator modifying signature # Backcompat decorator which does not modify runtime reported signature -matplotlib.offsetbox.*Offset[Bb]ox.get_offset +matplotlib\.offsetbox\..*Offset[Bb]ox\.get_offset # Inconsistent super/sub class parameter name (maybe rename for consistency) -matplotlib.projections.polar.RadialLocator.nonsingular -matplotlib.ticker.LogLocator.nonsingular -matplotlib.ticker.LogitLocator.nonsingular +matplotlib\.projections\.polar\.RadialLocator\.nonsingular +matplotlib\.ticker\.LogLocator\.nonsingular +matplotlib\.ticker\.LogitLocator\.nonsingular # Stdlib/Enum considered inconsistent (no fault of ours, I don't think) -matplotlib.backend_bases._Mode.__new__ -matplotlib.units.Number.__hash__ +matplotlib\.backend_bases\._Mode\.__new__ +matplotlib\.units\.Number\.__hash__ # 3.6 Pending deprecations -matplotlib.figure.Figure.set_constrained_layout -matplotlib.figure.Figure.set_constrained_layout_pads -matplotlib.figure.Figure.set_tight_layout +matplotlib\.figure\.Figure\.set_constrained_layout +matplotlib\.figure\.Figure\.set_constrained_layout_pads +matplotlib\.figure\.Figure\.set_tight_layout # Maybe should be abstractmethods, required for subclasses, stubs define once -matplotlib.tri.*TriInterpolator.__call__ -matplotlib.tri.*TriInterpolator.gradient +matplotlib\.tri\..*TriInterpolator\.__call__ +matplotlib\.tri\..*TriInterpolator\.gradient # TypeVar used only in type hints -matplotlib.backend_bases.FigureCanvasBase._T -matplotlib.backend_managers.ToolManager._T -matplotlib.spines.Spine._T +matplotlib\.backend_bases\.FigureCanvasBase\._T +matplotlib\.backend_managers\.ToolManager\._T +matplotlib\.spines\.Spine\._T # Parameter inconsistency due to 3.10 deprecation -matplotlib.figure.FigureBase.get_figure +matplotlib\.figure\.FigureBase\.get_figure # getitem method only exists for 3.10 deprecation backcompatability -matplotlib.inset.InsetIndicator.__getitem__ +matplotlib\.inset\.InsetIndicator\.__getitem__ diff --git a/doc/_embedded_plots/figure_subplots_adjust.py b/doc/_embedded_plots/figure_subplots_adjust.py new file mode 100644 index 000000000000..b4b8d7d32a3d --- /dev/null +++ b/doc/_embedded_plots/figure_subplots_adjust.py @@ -0,0 +1,28 @@ +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) +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 +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") + +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") +fig.text(0.28, 0.5, "hspace", va="center") +fig.text(0.28, 0.95, "top", va="center") diff --git a/doc/api/axes_api.rst b/doc/api/axes_api.rst index ac4e5bc4f536..6afdad4e768e 100644 --- a/doc/api/axes_api.rst +++ b/doc/api/axes_api.rst @@ -28,6 +28,17 @@ The Axes class Axes +Attributes +---------- + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + Axes.viewLim + Axes.dataLim + Plotting ======== diff --git a/doc/api/axis_api.rst b/doc/api/axis_api.rst index 424213445169..0870d38c9439 100644 --- a/doc/api/axis_api.rst +++ b/doc/api/axis_api.rst @@ -73,10 +73,10 @@ Axis Label :template: autosummary.rst :nosignatures: + Axis.label Axis.set_label_coords Axis.set_label_position Axis.set_label_text - Axis.get_label Axis.get_label_position Axis.get_label_text @@ -235,6 +235,8 @@ specify a matching series of labels. Calling ``set_ticks`` makes a :template: autosummary.rst :nosignatures: + Axis.get_label + Axis.set_label Axis.set_ticks Axis.set_ticklabels diff --git a/doc/api/next_api_changes/behavior/26000-t.rst b/doc/api/next_api_changes/behavior/26000-t.rst new file mode 100644 index 000000000000..054feb0887e6 --- /dev/null +++ b/doc/api/next_api_changes/behavior/26000-t.rst @@ -0,0 +1,5 @@ +onselect argument to selector widgets made optional +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The *onselect* argument to `.EllipseSelector`, `.LassoSelector`, `.PolygonSelector`, and +`.RectangleSelector` is no longer required. diff --git a/doc/api/next_api_changes/deprecations/28843-TH.rst b/doc/api/next_api_changes/deprecations/28843-TH.rst new file mode 100644 index 000000000000..25dc91be3ccc --- /dev/null +++ b/doc/api/next_api_changes/deprecations/28843-TH.rst @@ -0,0 +1,9 @@ +FontProperties initialization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`.FontProperties` initialization is limited to the two call patterns: + +- single positional parameter, interpreted as fontconfig pattern +- only keyword parameters for setting individual properties + +All other previously supported call patterns are deprecated. diff --git a/doc/api/next_api_changes/deprecations/28933-AL.rst b/doc/api/next_api_changes/deprecations/28933-AL.rst new file mode 100644 index 000000000000..b551c124b4e0 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/28933-AL.rst @@ -0,0 +1,5 @@ +``AxLine`` ``xy1`` and ``xy2`` setters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +These setters now each take a single argument, ``xy1`` or ``xy2`` as a tuple. +The old form, where ``x`` and ``y`` were passed as separate arguments, is +deprecated. diff --git a/doc/api/next_api_changes/removals/28874-ES.rst b/doc/api/next_api_changes/removals/28874-ES.rst new file mode 100644 index 000000000000..dbd8778dead1 --- /dev/null +++ b/doc/api/next_api_changes/removals/28874-ES.rst @@ -0,0 +1,180 @@ +Passing extra positional arguments to ``Figure.add_axes`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Positional arguments passed to `.Figure.add_axes` other than a rect or an existing +``Axes`` were previously ignored, and is now an error. + + +Artists explicitly passed in will no longer be filtered by legend() based on their label +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, artists explicitly passed to ``legend(handles=[...])`` are filtered out if +their label starts with an underscore. This filter is no longer applied; explicitly +filter out such artists (``[art for art in artists if not +art.get_label().startswith('_')]``) if necessary. + +Note that if no handles are specified at all, then the default still filters out labels +starting with an underscore. + + +The parameter of ``Annotation.contains`` and ``Legend.contains`` is renamed to *mouseevent* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... consistently with `.Artist.contains`. + + +Support for passing the "frac" key in ``annotate(..., arrowprops={"frac": ...})`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... has been removed. This key has had no effect since Matplotlib 1.5. + + +Passing non-int or sequence of non-int to ``Table.auto_set_column_width`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Column numbers are ints, and formerly passing any other type was effectively ignored. +This has now become an error. + + +Widgets +~~~~~~~ + +The *visible* attribute getter of ``*Selector`` widgets has been removed; use +``get_visible`` instead. + + +Auto-closing of figures when switching backend +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Allowable backend switches (i.e. those that do not swap a GUI event loop with another +one) will not close existing figures. If necessary, call ``plt.close("all")`` before +switching. + + +``FigureCanvasBase.switch_backends`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... has been removed with no replacement. + + +Accessing ``event.guiEvent`` after event handlers return +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... is no longer supported, and ``event.guiEvent`` will be set to None once the event +handlers return. For some GUI toolkits, it is unsafe to use the event, though you may +separately stash the object at your own risk. + + +``PdfPages(keep_empty=True)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A zero-page PDF is not valid, thus passing ``keep_empty=True`` to `.backend_pdf.PdfPages` +and `.backend_pgf.PdfPages`, and the ``keep_empty`` attribute of these classes, is no +longer allowed, and empty PDF files will not be created. + +Furthermore, `.backend_pdf.PdfPages` no longer immediately creates the target file upon +instantiation, but only when the first figure is saved. To fully control file creation, +directly pass an opened file object as argument (``with open(path, "wb") as file, +PdfPages(file) as pdf: ...``). + + +``backend_ps.psDefs`` +~~~~~~~~~~~~~~~~~~~~~ + +The ``psDefs`` module-level variable in ``backend_ps`` has been removed with no +replacement. + + +Automatic papersize selection in PostScript +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Setting :rc:`ps.papersize` to ``'auto'`` or passing ``papersize='auto'`` to +`.Figure.savefig` is no longer supported. Either pass an explicit paper type name, or +omit this parameter to use the default from the rcParam. + + +``RendererAgg.tostring_rgb`` and ``FigureCanvasAgg.tostring_rgb`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... have been remove with no direct replacement. Consider using ``buffer_rgba`` instead, +which should cover most use cases. + + +``NavigationToolbar2QT.message`` has been removed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... with no replacement. + + +``TexManager.texcache`` +~~~~~~~~~~~~~~~~~~~~~~~ + +... is considered private and has been removed. The location of the cache directory is +clarified in the doc-string. + + +``cbook`` API changes +~~~~~~~~~~~~~~~~~~~~~ + +``cbook.Stack`` has been removed with no replacement. + +``Grouper.clean()`` has been removed with no replacement. The Grouper class now cleans +itself up automatically. + +The *np_load* parameter of ``cbook.get_sample_data`` has been removed; `.get_sample_data` +now auto-loads numpy arrays. Use ``get_sample_data(..., asfileobj=False)`` instead to get +the filename of the data file, which can then be passed to `open`, if desired. + + +Calling ``paths.get_path_collection_extents`` with empty *offsets* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Calling `~.get_path_collection_extents` with an empty *offsets* parameter has an +ambiguous interpretation and is no longer allowed. + + +``bbox.anchored()`` with no explicit container +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Not passing a *container* argument to `.BboxBase.anchored` is no longer supported. + + +``INVALID_NON_AFFINE``, ``INVALID_AFFINE``, ``INVALID`` attributes of ``TransformNode`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These attributes have been removed. + + +``axes_grid1`` API changes +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``anchored_artists.AnchoredEllipse`` has been removed. Instead, directly construct an +`.AnchoredOffsetbox`, an `.AuxTransformBox`, and an `~.patches.Ellipse`, as demonstrated +in :doc:`/gallery/misc/anchored_artists`. + +The ``axes_divider.AxesLocator`` class has been removed. The ``new_locator`` method of +divider instances now instead returns an opaque callable (which can still be passed to +``ax.set_axes_locator``). + +``axes_divider.Divider.locate`` has been removed; use ``Divider.new_locator(...)(ax, +renderer)`` instead. + +``axes_grid.CbarAxesBase.toggle_label`` has been removed. Instead, use standard methods +for manipulating colorbar labels (`.Colorbar.set_label`) and tick labels +(`.Axes.tick_params`). + +``inset_location.InsetPosition`` has been removed; use `~.Axes.inset_axes` instead. + + +``axisartist`` API changes +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``axisartist.axes_grid`` and ``axisartist.axes_rgb`` modules, which provide wrappers +combining the functionality of `.axes_grid1` and `.axisartist`, have been removed; +directly use e.g. ``AxesGrid(..., axes_class=axislines.Axes)`` instead. + +Calling an axisartist Axes to mean `~matplotlib.pyplot.axis` has been removed; explicitly +call the method instead. + +``floating_axes.GridHelperCurveLinear.get_data_boundary`` has been removed. Use +``grid_finder.extreme_finder(*[None] * 5)`` to get the extremes of the grid. diff --git a/doc/api/prev_api_changes/api_changes_2.2.0.rst b/doc/api/prev_api_changes/api_changes_2.2.0.rst index 83369b66f8ab..404d0ca3ba38 100644 --- a/doc/api/prev_api_changes/api_changes_2.2.0.rst +++ b/doc/api/prev_api_changes/api_changes_2.2.0.rst @@ -61,7 +61,7 @@ the future, only broadcasting (1 column to *n* columns) will be performed. rcparams ~~~~~~~~ -The :rc:`backend.qt4` and :rc:`backend.qt5` rcParams were deprecated +The ``backend.qt4`` and ``backend.qt5`` rcParams were deprecated in version 2.2. In order to force the use of a specific Qt binding, either import that binding first, or set the ``QT_API`` environment variable. diff --git a/doc/api/prev_api_changes/api_changes_3.1.0.rst b/doc/api/prev_api_changes/api_changes_3.1.0.rst index 5b06af781938..365476f54e3c 100644 --- a/doc/api/prev_api_changes/api_changes_3.1.0.rst +++ b/doc/api/prev_api_changes/api_changes_3.1.0.rst @@ -743,8 +743,8 @@ The following signature related behaviours are deprecated: `.Axes.annotate()` instead. - Passing (n, 1)-shaped error arrays to `.Axes.errorbar()`, which was not documented and did not work for ``n = 2``. Pass a 1D array instead. -- The *frameon* kwarg to `~.Figure.savefig` and the :rc:`savefig.frameon` rcParam. - To emulate ``frameon = False``, set *facecolor* to fully +- The *frameon* keyword argument to `~.Figure.savefig` and the ``savefig.frameon`` + rcParam. To emulate ``frameon = False``, set *facecolor* to fully transparent (``"none"``, or ``(0, 0, 0, 0)``). - Passing a non-1D (typically, (n, 1)-shaped) input to `.Axes.pie`. Pass a 1D array instead. diff --git a/doc/api/prev_api_changes/api_changes_3.2.0/removals.rst b/doc/api/prev_api_changes/api_changes_3.2.0/removals.rst index 8e4c1e81f9ec..53d76d667509 100644 --- a/doc/api/prev_api_changes/api_changes_3.2.0/removals.rst +++ b/doc/api/prev_api_changes/api_changes_3.2.0/removals.rst @@ -61,7 +61,7 @@ The following API elements have been removed: - passing ``(verts, 0)`` or ``(..., 3)`` when specifying a marker to specify a path or a circle, respectively (instead, use ``verts`` or ``"o"``, respectively) -- :rc:`examples.directory` +- the ``examples.directory`` rcParam The following members of ``matplotlib.backends.backend_pdf.PdfFile`` were removed: diff --git a/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst index 256c33ed762f..76c43b12aaaa 100644 --- a/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst +++ b/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst @@ -83,8 +83,8 @@ Passing both singular and plural *colors*, *linewidths*, *linestyles* to `.Axes. Passing e.g. both *linewidth* and *linewidths* will raise a TypeError in the future. -Setting :rc:`text.latex.preamble` or :rc:`pdf.preamble` to non-strings -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Setting ``text.latex.preamble`` or ``pdf.preamble`` rcParams to non-strings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ These rcParams should be set to string values. Support for None (meaning the empty string) and lists of strings (implicitly joined with newlines) is deprecated. @@ -311,7 +311,7 @@ JPEG options ~~~~~~~~~~~~ The *quality*, *optimize*, and *progressive* keyword arguments to `~.Figure.savefig`, which were only used when saving to JPEG, are deprecated. -:rc:`savefig.jpeg_quality` is likewise deprecated. +The ``savefig.jpeg_quality`` rcParam is likewise deprecated. Such options should now be directly passed to Pillow using ``savefig(..., pil_kwargs={"quality": ..., "optimize": ..., "progressive": ...})``. diff --git a/doc/api/prev_api_changes/api_changes_3.5.0/removals.rst b/doc/api/prev_api_changes/api_changes_3.5.0/removals.rst index 0dcf76cbbe7a..3acab92c3577 100644 --- a/doc/api/prev_api_changes/api_changes_3.5.0/removals.rst +++ b/doc/api/prev_api_changes/api_changes_3.5.0/removals.rst @@ -359,7 +359,6 @@ rcParams - :rc:`axes.axisbelow` no longer accepts strings starting with "line" (case-insensitive) as "line"; use "line" (case-sensitive) instead. - - :rc:`text.latex.preamble` and :rc:`pdf.preamble` no longer accept - non-string values. + - :rc:`text.latex.preamble` and ``pdf.preamble`` no longer accept non-string values. - All ``*.linestyle`` rcParams no longer accept ``offset = None``; set the offset to 0 instead. diff --git a/doc/api/prev_api_changes/api_changes_3.6.0/behaviour.rst b/doc/api/prev_api_changes/api_changes_3.6.0/behaviour.rst index 91802692ebb4..6ace010515fb 100644 --- a/doc/api/prev_api_changes/api_changes_3.6.0/behaviour.rst +++ b/doc/api/prev_api_changes/api_changes_3.6.0/behaviour.rst @@ -241,7 +241,7 @@ Specified exception types in ``Grid`` In a few cases an `Exception` was thrown when an incorrect argument value was set in the `mpl_toolkits.axes_grid1.axes_grid.Grid` (= -`mpl_toolkits.axisartist.axes_grid.Grid`) constructor. These are replaced as +``mpl_toolkits.axisartist.axes_grid.Grid``) constructor. These are replaced as follows: * Providing an incorrect value for *ngrids* now raises a `ValueError` diff --git a/doc/api/prev_api_changes/api_changes_3.7.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.7.0/deprecations.rst index dd6d9d8e0894..55a0a7133c65 100644 --- a/doc/api/prev_api_changes/api_changes_3.7.0/deprecations.rst +++ b/doc/api/prev_api_changes/api_changes_3.7.0/deprecations.rst @@ -90,7 +90,7 @@ Passing undefined *label_mode* to ``Grid`` ... is deprecated. This includes `mpl_toolkits.axes_grid1.axes_grid.Grid`, `mpl_toolkits.axes_grid1.axes_grid.AxesGrid`, and `mpl_toolkits.axes_grid1.axes_grid.ImageGrid` as well as the corresponding -classes imported from `mpl_toolkits.axisartist.axes_grid`. +classes imported from ``mpl_toolkits.axisartist.axes_grid``. Pass ``label_mode='keep'`` instead to get the previous behavior of not modifying labels. diff --git a/doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst b/doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst index 3476a05394df..0b598723e26c 100644 --- a/doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst +++ b/doc/api/prev_api_changes/api_changes_3.8.0/behaviour.rst @@ -159,10 +159,10 @@ Passing ``None`` as ``rotation_mode`` to `.Text` (the default value) or passing `.Text.set_rotation_mode` will make `.Text.get_rotation_mode` return ``"default"`` instead of ``None``. The behaviour otherwise is the same. -PostScript paper type adds option to use figure size +PostScript paper size adds option to use figure size ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The :rc:`ps.papertype` rcParam can now be set to ``'figure'``, which will use +The :rc:`ps.papersize` rcParam can now be set to ``'figure'``, which will use a paper size that corresponds exactly with the size of the figure that is being saved. diff --git a/doc/api/prev_api_changes/api_changes_3.9.0/removals.rst b/doc/api/prev_api_changes/api_changes_3.9.0/removals.rst index b9aa03cfbf92..791c04149981 100644 --- a/doc/api/prev_api_changes/api_changes_3.9.0/removals.rst +++ b/doc/api/prev_api_changes/api_changes_3.9.0/removals.rst @@ -111,7 +111,7 @@ Passing undefined *label_mode* to ``Grid`` ... is no longer allowed. This includes `mpl_toolkits.axes_grid1.axes_grid.Grid`, `mpl_toolkits.axes_grid1.axes_grid.AxesGrid`, and `mpl_toolkits.axes_grid1.axes_grid.ImageGrid` as well as the corresponding classes -imported from `mpl_toolkits.axisartist.axes_grid`. +imported from ``mpl_toolkits.axisartist.axes_grid``. Pass ``label_mode='keep'`` instead to get the previous behavior of not modifying labels. diff --git a/doc/api/testing_api.rst b/doc/api/testing_api.rst index 7731d4510b27..ae81d2f89ca7 100644 --- a/doc/api/testing_api.rst +++ b/doc/api/testing_api.rst @@ -37,3 +37,11 @@ :members: :undoc-members: :show-inheritance: + + +Testing with optional dependencies +================================== +For more information on fixtures, see :external+pytest:ref:`pytest fixtures `. + +.. autofunction:: matplotlib.testing.conftest.pd +.. autofunction:: matplotlib.testing.conftest.xr diff --git a/doc/api/toolkits/axisartist.rst b/doc/api/toolkits/axisartist.rst index 8cac4d68a266..5f58d134d370 100644 --- a/doc/api/toolkits/axisartist.rst +++ b/doc/api/toolkits/axisartist.rst @@ -34,8 +34,6 @@ You can find a tutorial describing usage of axisartist at the axisartist.angle_helper axisartist.axes_divider - axisartist.axes_grid - axisartist.axes_rgb axisartist.axis_artist axisartist.axisline_style axisartist.axislines diff --git a/doc/conf.py b/doc/conf.py index ea6b1a3fa444..c9d498e939f7 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -220,6 +220,7 @@ def tutorials_download_error(record): autosummary_generate = True autodoc_typehints = "none" +autodoc_mock_imports = ["pytest"] # we should ignore warnings coming from importing deprecated modules for # autodoc purposes, as this will disappear automatically when they are removed diff --git a/doc/devel/MEP/MEP23.rst b/doc/devel/MEP/MEP23.rst index d6b342877959..ec56f362c867 100644 --- a/doc/devel/MEP/MEP23.rst +++ b/doc/devel/MEP/MEP23.rst @@ -38,8 +38,8 @@ desirable to be able to group these under the same window. See :ghpull:`2194`. The proposed solution modifies `.FigureManagerBase` to contain and manage more -than one ``Canvas``. The settings parameter :rc:`backend.multifigure` control -when the **MultiFigure** behaviour is desired. +than one ``Canvas``. The ``backend.multifigure`` rcParam controls when the +**MultiFigure** behaviour is desired. **Note** diff --git a/doc/devel/development_setup.rst b/doc/devel/development_setup.rst index 5ac78cea8c90..acd004690e8b 100644 --- a/doc/devel/development_setup.rst +++ b/doc/devel/development_setup.rst @@ -155,19 +155,20 @@ The simplest way to do this is to use either Python's virtual environment Remember to activate the environment whenever you start working on Matplotlib. -Install Dependencies -==================== +Install external dependencies +============================= -Most Python dependencies will be installed when :ref:`setting up the environment ` -but non-Python dependencies like C++ compilers, LaTeX, and other system applications -must be installed separately. +Python dependencies were installed as part of :ref:`setting up the environment `. +Additionally, the following non-Python dependencies must also be installed: -.. toctree:: - :maxdepth: 2 +.. rst-class:: checklist - ../install/dependencies +* :ref:`c++ compiler` +* :ref:`documentation build dependencies ` +For a full list of dependencies, see :ref:`dependencies`. + .. _development-install: Install Matplotlib in editable mode diff --git a/doc/install/dependencies.rst b/doc/install/dependencies.rst index 3d921d2d10c9..988839114bae 100644 --- a/doc/install/dependencies.rst +++ b/doc/install/dependencies.rst @@ -250,37 +250,38 @@ development environment that must be installed before a compiler can be installe You may also need to install headers for various libraries used in the compiled extension source files. +.. _dev-compiler: .. tab-set:: - .. tab-item:: Linux + .. tab-item:: Linux - On some Linux systems, you can install a meta-build package. For example, - on Ubuntu ``apt install build-essential`` + On some Linux systems, you can install a meta-build package. For example, + on Ubuntu ``apt install build-essential`` - Otherwise, use the system distribution's package manager to install - :ref:`gcc `. + Otherwise, use the system distribution's package manager to install + :ref:`gcc `. - .. tab-item:: macOS + .. tab-item:: macOS - Install `Xcode `_ for Apple platform development. + Install `Xcode `_ for Apple platform development. - .. tab-item:: Windows + .. tab-item:: Windows - Install `Visual Studio Build Tools `_ + Install `Visual Studio Build Tools `_ - Make sure "Desktop development with C++" is selected, and that the latest MSVC, - "C++ CMake tools for Windows," and a Windows SDK compatible with your version - of Windows are selected and installed. They should be selected by default under - the "Optional" subheading, but are required to build Matplotlib from source. + Make sure "Desktop development with C++" is selected, and that the latest MSVC, + "C++ CMake tools for Windows," and a Windows SDK compatible with your version + of Windows are selected and installed. They should be selected by default under + the "Optional" subheading, but are required to build Matplotlib from source. - Alternatively, you can install a Linux-like environment such as `CygWin `_ - or `Windows Subsystem for Linux `_. - If using `MinGW-64 `_, we require **v6** of the - ```Mingw-w64-x86_64-headers``. + Alternatively, you can install a Linux-like environment such as `CygWin `_ + or `Windows Subsystem for Linux `_. + If using `MinGW-64 `_, we require **v6** of the + ```Mingw-w64-x86_64-headers``. -We highly recommend that you install a compiler using your platform tool, i.e., -Xcode, VS Code or Linux package manager. Choose **one** compiler from this list: +We highly recommend that you install a compiler using your platform tool, i.e., Xcode, +VS Code or Linux package manager. Choose **one** compiler from this list: .. _compiler-table: @@ -307,7 +308,6 @@ Xcode, VS Code or Linux package manager. Choose **one** compiler from this list: - `Visual Studio 2019 C++ `_ - .. _test-dependencies: Test dependencies @@ -327,8 +327,11 @@ Optional In addition to all of the optional dependencies on the main library, for testing the following will be used if they are installed. -- Ghostscript_ (>= 9.0, to render PDF files) -- Inkscape_ (to render SVG files) +Python +^^^^^^ +These packages are installed when :ref:`creating a virtual environment `, +otherwise they must be installed manually: + - nbformat_ and nbconvert_ used to test the notebook backend - pandas_ used to test compatibility with Pandas - pikepdf_ used in some tests for the pgf and pdf backends @@ -340,9 +343,14 @@ testing the following will be used if they are installed. - pytest-xvfb_ to run tests without windows popping up (Linux) - pytz_ used to test pytz int - sphinx_ used to test our sphinx extensions +- xarray_ used to test compatibility with xarray + +External tools +^^^^^^^^^^^^^^ +- Ghostscript_ (>= 9.0, to render PDF files) +- Inkscape_ (to render SVG files) - `WenQuanYi Zen Hei`_ and `Noto Sans CJK`_ fonts for testing font fallback and non-Western fonts -- xarray_ used to test compatibility with xarray If any of these dependencies are not discovered, then the tests that rely on them will be skipped by pytest. @@ -355,6 +363,7 @@ them will be skipped by pytest. .. _Ghostscript: https://ghostscript.com/ .. _Inkscape: https://inkscape.org +.. _WenQuanYi Zen Hei: http://wenq.org/en/ .. _flake8: https://pypi.org/project/flake8/ .. _nbconvert: https://pypi.org/project/nbconvert/ .. _nbformat: https://pypi.org/project/nbformat/ @@ -369,7 +378,6 @@ them will be skipped by pytest. .. _pytest-xvfb: https://pypi.org/project/pytest-xvfb/ .. _pytest: http://doc.pytest.org/en/latest/ .. _sphinx: https://pypi.org/project/Sphinx/ -.. _WenQuanYi Zen Hei: http://wenq.org/en/ .. _Noto Sans CJK: https://fonts.google.com/noto/use .. _xarray: https://pypi.org/project/xarray/ @@ -394,14 +402,15 @@ The content of :file:`doc-requirements.txt` is also shown below: :literal: +.. _doc-dependencies-external: + External tools -------------- -The documentation requires LaTeX and Graphviz. These are not -Python packages and must be installed separately. - Required ^^^^^^^^ +The documentation requires LaTeX and Graphviz. These are not +Python packages and must be installed separately. * `Graphviz `_ * a minimal working LaTeX distribution, e.g. `TeX Live `_ or @@ -409,15 +418,14 @@ Required The following LaTeX packages: - * `dvipng `_ - * `underscore `_ - * `cm-super `_ - * ``collection-fontsrecommended`` +* `dvipng `_ +* `underscore `_ +* `cm-super `_ +* ``collection-fontsrecommended`` The complete version of many LaTex distribution installers, e.g. "texlive-full" or "texlive-all", will often automatically include these packages. - Optional ^^^^^^^^ diff --git a/doc/missing-references.json b/doc/missing-references.json index 089434172e58..d1dc67b7e6a8 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -328,7 +328,7 @@ "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:212", "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Quiver:251", "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Path3DCollection.set:46", - "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Poly3DCollection.set:44" + "lib/mpl_toolkits/mplot3d/art3d.py:docstring of matplotlib.artist.Poly3DCollection.set:45" ], "matplotlib.collections._MeshData.set_array": [ "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolormesh:164", @@ -341,10 +341,6 @@ "Artist.stale_callback": [ "doc/users/explain/figure/interactive_guide.rst:323" ], - "Axes.dataLim": [ - "doc/api/axes_api.rst:293::1", - "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.update_datalim:2" - ], "AxesBase": [ "doc/api/axes_api.rst:448::1", "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.add_child_axes:2" diff --git a/doc/users/next_whats_new/3d_clip_to_axis_limits.rst b/doc/users/next_whats_new/3d_clip_to_axis_limits.rst new file mode 100644 index 000000000000..d97ba1b675ba --- /dev/null +++ b/doc/users/next_whats_new/3d_clip_to_axis_limits.rst @@ -0,0 +1,30 @@ +Data in 3D plots can now be dynamically clipped to the axes view limits +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All 3D plotting functions now support the *axlim_clip* keyword argument, which +will clip the data to the axes view limits, hiding all data outside those +bounds. This clipping will be dynamically applied in real time while panning +and zooming. + +Please note that if one vertex of a line segment or 3D patch is clipped, then +the entire segment or patch will be hidden. Not being able to show partial +lines or patches such that they are "smoothly" cut off at the boundaries of the +view box is a limitation of the current renderer. + +.. plot:: + :include-source: true + :alt: Example of default behavior (left) and axlim_clip=True (right) + + import matplotlib.pyplot as plt + import numpy as np + + fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) + np.random.seed(1) + xyz = np.random.rand(25, 3) + + # Note that when a line has one vertex outside the view limits, the entire + # line is hidden. The same is true for 3D patches (not shown). + ax.plot(xyz[:, 0], xyz[:, 1], xyz[:, 2], '-o') + ax.plot(xyz[:, 0], xyz[:, 1], xyz[:, 2], '--*', axlim_clip=True) + ax.set(xlim=(0.25, 0.75), ylim=(0, 1), zlim=(0, 1)) + ax.legend(['axlim_clip=False (default)', 'axlim_clip=True']) diff --git a/doc/users/next_whats_new/axes_creation_speedup.rst b/doc/users/next_whats_new/axes_creation_speedup.rst new file mode 100644 index 000000000000..c9eaa48c0060 --- /dev/null +++ b/doc/users/next_whats_new/axes_creation_speedup.rst @@ -0,0 +1,4 @@ +Axes creation speedup +~~~~~~~~~~~~~~~~~~~~~ + +Creating an Axes is now 20-25% faster due to internal optimizations. diff --git a/doc/users/next_whats_new/increased_figure_limits.rst b/doc/users/next_whats_new/increased_figure_limits.rst new file mode 100644 index 000000000000..499701cbca38 --- /dev/null +++ b/doc/users/next_whats_new/increased_figure_limits.rst @@ -0,0 +1,9 @@ +Increased Figure limits with Agg renderer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Figures using the Agg renderer are now limited to 2**23 pixels in each +direction, instead of 2**16. Additionally, bugs that caused artists to not +render past 2**15 pixels horizontally have been fixed. + +Note that if you are using a GUI backend, it may have its own smaller limits +(which may themselves depend on screen size.) diff --git a/doc/users/next_whats_new/subfigures_change_order.rst b/doc/users/next_whats_new/subfigures_change_order.rst index 49a018a3fd96..e059d71755bc 100644 --- a/doc/users/next_whats_new/subfigures_change_order.rst +++ b/doc/users/next_whats_new/subfigures_change_order.rst @@ -1,3 +1,9 @@ +Subfigures no longer provisional +-------------------------------- + +The API on `.Figure.subfigures` and `.SubFigure` are now considered stable. + + Subfigures are now added in row-major order ------------------------------------------- diff --git a/doc/users/prev_whats_new/whats_new_1.3.rst b/doc/users/prev_whats_new/whats_new_1.3.rst index 10811632c5c4..af40f37f92b7 100644 --- a/doc/users/prev_whats_new/whats_new_1.3.rst +++ b/doc/users/prev_whats_new/whats_new_1.3.rst @@ -292,9 +292,9 @@ rcParam has been set, and will not retroactively affect already existing text objects. This brings their behavior in line with most other rcParams. -Added :rc:`savefig.jpeg_quality` -```````````````````````````````` -rcParam value :rc:`savefig.jpeg_quality` was added so that the user can +Added ``savefig.jpeg_quality`` rcParam +`````````````````````````````````````` +The ``savefig.jpeg_quality`` rcParam was added so that the user can configure the default quality used when a figure is written as a JPEG. The default quality is 95; previously, the default quality was 75. This change minimizes the artifacting inherent in JPEG images, diff --git a/doc/users/prev_whats_new/whats_new_1.5.rst b/doc/users/prev_whats_new/whats_new_1.5.rst index 039f65e2eba6..5bb4d4b9b5e9 100644 --- a/doc/users/prev_whats_new/whats_new_1.5.rst +++ b/doc/users/prev_whats_new/whats_new_1.5.rst @@ -190,8 +190,8 @@ Some parameters have been added, others have been improved. +---------------------------+--------------------------------------------------+ | Parameter | Description | +===========================+==================================================+ -|:rc:`xaxis.labelpad`, | mplot3d now respects these parameters | -|:rc:`yaxis.labelpad` | | +|``xaxis.labelpad``, | mplot3d now respects these attributes, which | +|``yaxis.labelpad`` | default to :rc:`axes.labelpad` | +---------------------------+--------------------------------------------------+ |:rc:`axes.labelpad` | Default space between the axis and the label | +---------------------------+--------------------------------------------------+ diff --git a/doc/users/prev_whats_new/whats_new_3.1.0.rst b/doc/users/prev_whats_new/whats_new_3.1.0.rst index 3d63768f9c7a..9f53435b89f6 100644 --- a/doc/users/prev_whats_new/whats_new_3.1.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.1.0.rst @@ -260,8 +260,7 @@ Default minor tick spacing was changed from 0.625 to 0.5 for major ticks spaced A public API has been added to `.EngFormatter` to control how the numbers in the ticklabels will be rendered. By default, *useMathText* evaluates to -:rc:`axes.formatter.use_mathtext'` and *usetex* evaluates to -:rc:`'text.usetex'`. +:rc:`axes.formatter.use_mathtext` and *usetex* evaluates to :rc:`text.usetex`. If either is `True` then the numbers will be encapsulated by ``$`` signs. When using ``TeX`` this implies that the numbers will be shown diff --git a/doc/users/prev_whats_new/whats_new_3.3.0.rst b/doc/users/prev_whats_new/whats_new_3.3.0.rst index 00ea10620d14..94914bcc75db 100644 --- a/doc/users/prev_whats_new/whats_new_3.3.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.3.0.rst @@ -292,8 +292,8 @@ positioning. For the xlabel, the supported values are 'left', 'center', or 'right'. For the ylabel, the supported values are 'bottom', 'center', or 'top'. -The default is controlled via :rc:`xaxis.labelposition` and -:rc:`yaxis.labelposition`; the Colorbar label takes the rcParam based on its +The default is controlled via :rc:`xaxis.labellocation` and +:rc:`yaxis.labellocation`; the Colorbar label takes the rcParam based on its orientation. .. plot:: diff --git a/doc/users/prev_whats_new/whats_new_3.6.0.rst b/doc/users/prev_whats_new/whats_new_3.6.0.rst index 859bbb47e354..9fcf8cebfc6f 100644 --- a/doc/users/prev_whats_new/whats_new_3.6.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.6.0.rst @@ -217,7 +217,7 @@ Linestyles for negative contours may be set individually The line style of negative contours may be set by passing the *negative_linestyles* argument to `.Axes.contour`. Previously, this style could -only be set globally via :rc:`contour.negative_linestyles`. +only be set globally via :rc:`contour.negative_linestyle`. .. plot:: :alt: Two contour plots, each showing two positive and two negative contours. The positive contours are shown in solid black lines in both plots. In one plot the negative contours are shown in dashed lines, which is the current styling. In the other plot they're shown in dotted lines, which is one of the new options. diff --git a/doc/users/prev_whats_new/whats_new_3.8.0.rst b/doc/users/prev_whats_new/whats_new_3.8.0.rst index 8c34252098db..88f987172adb 100644 --- a/doc/users/prev_whats_new/whats_new_3.8.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.8.0.rst @@ -496,7 +496,7 @@ Other improvements macosx: New figures can be opened in either windows or tabs ----------------------------------------------------------- -There is a new :rc:`macosx.window_mode`` rcParam to control how +There is a new :rc:`macosx.window_mode` rcParam to control how new figures are opened with the macosx backend. The default is **system** which uses the system settings, or one can specify either **tab** or **window** to explicitly choose the mode used to open new figures. diff --git a/extern/agg24-svn/include/agg_rasterizer_cells_aa.h b/extern/agg24-svn/include/agg_rasterizer_cells_aa.h index d1cc705405dc..44a55417b492 100644 --- a/extern/agg24-svn/include/agg_rasterizer_cells_aa.h +++ b/extern/agg24-svn/include/agg_rasterizer_cells_aa.h @@ -325,8 +325,10 @@ namespace agg if(dx >= dx_limit || dx <= -dx_limit) { - int cx = (x1 + x2) >> 1; - int cy = (y1 + y2) >> 1; + // These are overflow safe versions of (x1 + x2) >> 1; divide each by 2 + // first, then add 1 if both were odd. + int cx = (x1 >> 1) + (x2 >> 1) + ((x1 & 1) & (x2 & 1)); + int cy = (y1 >> 1) + (y2 >> 1) + ((y1 & 1) & (y2 & 1)); line(x1, y1, cx, cy); line(cx, cy, x2, y2); return; diff --git a/galleries/examples/axes_grid1/inset_locator_demo.py b/galleries/examples/axes_grid1/inset_locator_demo.py index fa9c4593d932..e4b310ac6c73 100644 --- a/galleries/examples/axes_grid1/inset_locator_demo.py +++ b/galleries/examples/axes_grid1/inset_locator_demo.py @@ -20,21 +20,21 @@ fig, (ax, ax2) = plt.subplots(1, 2, figsize=[5.5, 2.8]) # Create inset of width 1.3 inches and height 0.9 inches -# at the default upper right location +# at the default upper right location. axins = inset_axes(ax, width=1.3, height=0.9) # Create inset of width 30% and height 40% of the parent Axes' bounding box -# at the lower left corner (loc=3) -axins2 = inset_axes(ax, width="30%", height="40%", loc=3) +# at the lower left corner. +axins2 = inset_axes(ax, width="30%", height="40%", loc="lower left") # Create inset of mixed specifications in the second subplot; # width is 30% of parent Axes' bounding box and -# height is 1 inch at the upper left corner (loc=2) -axins3 = inset_axes(ax2, width="30%", height=1., loc=2) +# height is 1 inch at the upper left corner. +axins3 = inset_axes(ax2, width="30%", height=1., loc="upper left") -# Create an inset in the lower right corner (loc=4) with borderpad=1, i.e. -# 10 points padding (as 10pt is the default fontsize) to the parent Axes -axins4 = inset_axes(ax2, width="20%", height="20%", loc=4, borderpad=1) +# Create an inset in the lower right corner with borderpad=1, i.e. +# 10 points padding (as 10pt is the default fontsize) to the parent Axes. +axins4 = inset_axes(ax2, width="20%", height="20%", loc="lower right", borderpad=1) # Turn ticklabels of insets off for axi in [axins, axins2, axins3, axins4]: @@ -61,12 +61,12 @@ # in those coordinates. # Inside this bounding box an inset of half the bounding box' width and # three quarters of the bounding box' height is created. The lower left corner -# of the inset is aligned to the lower left corner of the bounding box (loc=3). +# of the inset is aligned to the lower left corner of the bounding box. # The inset is then offset by the default 0.5 in units of the font size. axins = inset_axes(ax, width="50%", height="75%", bbox_to_anchor=(.2, .4, .6, .5), - bbox_transform=ax.transAxes, loc=3) + bbox_transform=ax.transAxes, loc="lower left") # For visualization purposes we mark the bounding box by a rectangle ax.add_patch(plt.Rectangle((.2, .4), .6, .5, ls="--", ec="c", fc="none", @@ -113,7 +113,7 @@ # Create an inset outside the Axes axins = inset_axes(ax, width="100%", height="100%", bbox_to_anchor=(1.05, .6, .5, .4), - bbox_transform=ax.transAxes, loc=2, borderpad=0) + bbox_transform=ax.transAxes, loc="upper left", borderpad=0) axins.tick_params(left=False, right=True, labelleft=False, labelright=True) # Create an inset with a 2-tuple bounding box. Note that this creates a @@ -121,7 +121,7 @@ # width and height in absolute units (inches). axins2 = inset_axes(ax, width=0.5, height=0.4, bbox_to_anchor=(0.33, 0.25), - bbox_transform=ax.transAxes, loc=3, borderpad=0) + bbox_transform=ax.transAxes, loc="lower left", borderpad=0) ax2 = fig.add_subplot(133) @@ -131,7 +131,7 @@ # Create inset in data coordinates using ax.transData as transform axins3 = inset_axes(ax2, width="100%", height="100%", bbox_to_anchor=(1e-2, 2, 1e3, 3), - bbox_transform=ax2.transData, loc=2, borderpad=0) + bbox_transform=ax2.transData, loc="upper left", borderpad=0) # Create an inset horizontally centered in figure coordinates and vertically # bound to line up with the Axes. @@ -140,6 +140,6 @@ transform = blended_transform_factory(fig.transFigure, ax2.transAxes) axins4 = inset_axes(ax2, width="16%", height="34%", bbox_to_anchor=(0, 0, 1, 1), - bbox_transform=transform, loc=8, borderpad=0) + bbox_transform=transform, loc="lower center", borderpad=0) plt.show() diff --git a/galleries/examples/axes_grid1/inset_locator_demo2.py b/galleries/examples/axes_grid1/inset_locator_demo2.py index f648c38e8d55..1bbbdd39b886 100644 --- a/galleries/examples/axes_grid1/inset_locator_demo2.py +++ b/galleries/examples/axes_grid1/inset_locator_demo2.py @@ -36,7 +36,7 @@ def add_sizebar(ax, size): asb = AnchoredSizeBar(ax.transData, size, str(size), - loc=8, + loc="lower center", pad=0.1, borderpad=0.5, sep=5, frameon=False) ax.add_artist(asb) @@ -54,7 +54,7 @@ def add_sizebar(ax, size): ax2.imshow(Z2, extent=extent, origin="lower") -axins2 = zoomed_inset_axes(ax2, zoom=6, loc=1) +axins2 = zoomed_inset_axes(ax2, zoom=6, loc="upper right") axins2.imshow(Z2, extent=extent, origin="lower") # subregion of the original image diff --git a/galleries/examples/axes_grid1/parasite_simple.py b/galleries/examples/axes_grid1/parasite_simple.py index ad4922308a3f..a0c4d68051a9 100644 --- a/galleries/examples/axes_grid1/parasite_simple.py +++ b/galleries/examples/axes_grid1/parasite_simple.py @@ -20,7 +20,7 @@ host.legend(labelcolor="linecolor") -host.yaxis.get_label().set_color(p1.get_color()) -par.yaxis.get_label().set_color(p2.get_color()) +host.yaxis.label.set_color(p1.get_color()) +par.yaxis.label.set_color(p2.get_color()) plt.show() diff --git a/galleries/examples/color/color_cycle_default.py b/galleries/examples/color/color_cycle_default.py index 16f6634937c0..af35f6d00f9e 100644 --- a/galleries/examples/color/color_cycle_default.py +++ b/galleries/examples/color/color_cycle_default.py @@ -9,33 +9,28 @@ import matplotlib.pyplot as plt import numpy as np -prop_cycle = plt.rcParams['axes.prop_cycle'] -colors = prop_cycle.by_key()['color'] +from matplotlib.colors import TABLEAU_COLORS, same_color + -lwbase = plt.rcParams['lines.linewidth'] -thin = lwbase / 2 -thick = lwbase * 3 +def f(x, a): + """A nice sigmoid-like parametrized curve, ending approximately at *a*.""" + return 0.85 * a * (1 / (1 + np.exp(-x)) + 0.2) -fig, axs = plt.subplots(nrows=2, ncols=2, sharex=True, sharey=True) -for icol in range(2): - if icol == 0: - lwx, lwy = thin, lwbase - else: - lwx, lwy = lwbase, thick - for irow in range(2): - for i, color in enumerate(colors): - axs[irow, icol].axhline(i, color=color, lw=lwx) - axs[irow, icol].axvline(i, color=color, lw=lwy) - axs[1, icol].set_facecolor('k') - axs[1, icol].xaxis.set_ticks(np.arange(0, 10, 2)) - axs[0, icol].set_title(f'line widths (pts): {lwx:g}, {lwy:g}', - fontsize='medium') +fig, ax = plt.subplots() +ax.axis('off') +ax.set_title("Colors in the default property cycle") -for irow in range(2): - axs[irow, 0].yaxis.set_ticks(np.arange(0, 10, 2)) +prop_cycle = plt.rcParams['axes.prop_cycle'] +colors = prop_cycle.by_key()['color'] +x = np.linspace(-4, 4, 200) -fig.suptitle('Colors in the default prop_cycle', fontsize='large') +for i, (color, color_name) in enumerate(zip(colors, TABLEAU_COLORS)): + assert same_color(color, color_name) + pos = 4.5 - i + ax.plot(x, f(x, pos)) + ax.text(4.2, pos, f"'C{i}': '{color_name}'", color=color, va="center") + ax.bar(9, 1, width=1.5, bottom=pos-0.5) plt.show() @@ -46,14 +41,14 @@ # The use of the following functions, methods, classes and modules is shown # in this example: # -# - `matplotlib.axes.Axes.axhline` / `matplotlib.pyplot.axhline` -# - `matplotlib.axes.Axes.axvline` / `matplotlib.pyplot.axvline` -# - `matplotlib.axes.Axes.set_facecolor` -# - `matplotlib.figure.Figure.suptitle` +# - `matplotlib.axes.Axes.axis` +# - `matplotlib.axes.Axes.text` +# - `matplotlib.colors.same_color` +# - `cycler.Cycler` # # .. tags:: # # styling: color -# styling: colormap +# purpose: reference # plot-type: line # level: beginner diff --git a/galleries/examples/mplot3d/axlim_clip.py b/galleries/examples/mplot3d/axlim_clip.py new file mode 100644 index 000000000000..b25c55a30ad1 --- /dev/null +++ b/galleries/examples/mplot3d/axlim_clip.py @@ -0,0 +1,31 @@ +""" +===================================== +Clip the data to the axes view limits +===================================== + +Demonstrate clipping of line and marker data to the axes view limits. The +``axlim_clip`` keyword argument can be used in any of the 3D plotting +functions. +""" + +import matplotlib.pyplot as plt +import numpy as np + +fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) + +# Generate the random data +np.random.seed(1) +xyz = np.random.rand(25, 3) + +# Default behavior is axlim_clip=False +ax.plot(xyz[:, 0], xyz[:, 1], xyz[:, 2], '-o') + +# When axlim_clip=True, note that when a line segment has one vertex outside +# the view limits, the entire line is hidden. The same is true for 3D patches +# if one of their vertices is outside the limits (not shown). +ax.plot(xyz[:, 0], xyz[:, 1], xyz[:, 2], '--*', axlim_clip=True) + +ax.set(xlim=(0.25, 0.75), ylim=(0, 1), zlim=(-1, 1)) +ax.legend(['axlim_clip=False (default)', 'axlim_clip=True']) + +plt.show() diff --git a/galleries/examples/shapes_and_collections/fancybox_demo.py b/galleries/examples/shapes_and_collections/fancybox_demo.py index 91cc1d1749ea..8d36a5a14d9d 100644 --- a/galleries/examples/shapes_and_collections/fancybox_demo.py +++ b/galleries/examples/shapes_and_collections/fancybox_demo.py @@ -3,7 +3,8 @@ Drawing fancy boxes =================== -The following examples show how to plot boxes with different visual properties. +The following examples show how to plot boxes (`.FancyBboxPatch`) with different +visual properties. """ import inspect @@ -15,7 +16,12 @@ import matplotlib.transforms as mtransforms # %% -# First we'll show some sample boxes with fancybox. +# Box styles +# ---------- +# `.FancyBboxPatch` supports different `.BoxStyle`\s. Note that `~.Axes.text` +# allows to draw a box around the text by adding the ``bbox`` parameter. Therefore, +# you don't see explicit `.FancyBboxPatch` and `.BoxStyle` calls in the following +# example. styles = mpatch.BoxStyle.get_styles() ncol = 2 @@ -41,13 +47,21 @@ # %% -# Next we'll show off multiple fancy boxes at once. - +# Parameters for modifying the box +# -------------------------------- +# `.BoxStyle`\s have additional parameters to configure their appearance. +# For example, "round" boxes can have ``pad`` and ``rounding``. +# +# Additionally, the `.FancyBboxPatch` parameters ``mutation_scale`` and +# ``mutation_aspect`` scale the box appearance. def add_fancy_patch_around(ax, bb, **kwargs): - fancy = FancyBboxPatch(bb.p0, bb.width, bb.height, - fc=(1, 0.8, 1, 0.5), ec=(1, 0.5, 1, 0.5), - **kwargs) + kwargs = { + 'facecolor': (1, 0.8, 1, 0.5), + 'edgecolor': (1, 0.5, 1, 0.5), + **kwargs + } + fancy = FancyBboxPatch(bb.p0, bb.width, bb.height, **kwargs) ax.add_patch(fancy) return fancy @@ -65,7 +79,7 @@ def draw_control_points_for_patches(ax): ax = axs[0, 0] # a fancy box with round corners. pad=0.1 -fancy = add_fancy_patch_around(ax, bb, boxstyle="round,pad=0.1") +add_fancy_patch_around(ax, bb, boxstyle="round,pad=0.1") ax.set(xlim=(0, 1), ylim=(0, 1), aspect=1, title='boxstyle="round,pad=0.1"') @@ -84,33 +98,61 @@ def draw_control_points_for_patches(ax): ax = axs[1, 0] # mutation_scale determines the overall scale of the mutation, i.e. both pad # and rounding_size is scaled according to this value. -fancy = add_fancy_patch_around( - ax, bb, boxstyle="round,pad=0.1", mutation_scale=2) +add_fancy_patch_around(ax, bb, boxstyle="round,pad=0.1", mutation_scale=2) ax.set(xlim=(0, 1), ylim=(0, 1), aspect=1, title='boxstyle="round,pad=0.1"\n mutation_scale=2') ax = axs[1, 1] -# When the aspect ratio of the Axes is not 1, the fancy box may not be what you -# expected (green). -fancy = add_fancy_patch_around(ax, bb, boxstyle="round,pad=0.2") -fancy.set(facecolor="none", edgecolor="green") -# You can compensate this by setting the mutation_aspect (pink). -fancy = add_fancy_patch_around( - ax, bb, boxstyle="round,pad=0.3", mutation_aspect=0.5) -ax.set(xlim=(-.5, 1.5), ylim=(0, 1), aspect=2, - title='boxstyle="round,pad=0.3"\nmutation_aspect=.5') +# mutation_aspect scales the vertical influence of the parameters (technically, +# it scales the height of the box down by mutation_aspect, applies the box parameters +# and scales the result back up). In effect, the vertical pad is scaled to +# pad * mutation_aspect, e.g. mutation_aspect=0.5 halves the vertical pad. +add_fancy_patch_around(ax, bb, boxstyle="round,pad=0.1", mutation_aspect=0.5) +ax.set(xlim=(0, 1), ylim=(0, 1), + title='boxstyle="round,pad=0.1"\nmutation_aspect=0.5') for ax in axs.flat: draw_control_points_for_patches(ax) # Draw the original bbox (using boxstyle=square with pad=0). - fancy = add_fancy_patch_around(ax, bb, boxstyle="square,pad=0") - fancy.set(edgecolor="black", facecolor="none", zorder=10) + add_fancy_patch_around(ax, bb, boxstyle="square,pad=0", + edgecolor="black", facecolor="none", zorder=10) fig.tight_layout() plt.show() +# %% +# Creating visually constant padding on non-equal aspect Axes +# ----------------------------------------------------------- +# Since padding is in box coordinates, i.e. usually data coordinates, +# a given padding is rendered to different visual sizes if the +# Axes aspect is not 1. +# To get visually equal vertical and horizontal padding, set the +# mutation_aspect to the inverse of the Axes aspect. This scales +# the vertical padding appropriately. + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(6.5, 5)) + +# original boxes +bb = mtransforms.Bbox([[-0.5, -0.5], [0.5, 0.5]]) +add_fancy_patch_around(ax1, bb, boxstyle="square,pad=0", + edgecolor="black", facecolor="none", zorder=10) +add_fancy_patch_around(ax2, bb, boxstyle="square,pad=0", + edgecolor="black", facecolor="none", zorder=10) +ax1.set(xlim=(-1.5, 1.5), ylim=(-1.5, 1.5), aspect=2) +ax2.set(xlim=(-1.5, 1.5), ylim=(-1.5, 1.5), aspect=2) + + +fancy = add_fancy_patch_around( + ax1, bb, boxstyle="round,pad=0.5") +ax1.set_title("aspect=2\nmutation_aspect=1") + +fancy = add_fancy_patch_around( + ax2, bb, boxstyle="round,pad=0.5", mutation_aspect=0.5) +ax2.set_title("aspect=2\nmutation_aspect=0.5") + + # %% # # .. admonition:: References diff --git a/galleries/examples/subplots_axes_and_figures/subfigures.py b/galleries/examples/subplots_axes_and_figures/subfigures.py index 5060946b59b2..cbe62f57d6b1 100644 --- a/galleries/examples/subplots_axes_and_figures/subfigures.py +++ b/galleries/examples/subplots_axes_and_figures/subfigures.py @@ -13,9 +13,6 @@ `matplotlib.figure.Figure.subfigures` to make an array of subfigures. Note that subfigures can also have their own child subfigures. -.. note:: - The *subfigure* concept is new in v3.4, and the API is still provisional. - """ import matplotlib.pyplot as plt import numpy as np diff --git a/galleries/users_explain/toolkits/mplot3d.rst b/galleries/users_explain/toolkits/mplot3d.rst index 100449f23a0e..b4ddc48790cb 100644 --- a/galleries/users_explain/toolkits/mplot3d.rst +++ b/galleries/users_explain/toolkits/mplot3d.rst @@ -121,6 +121,8 @@ See `.Axes3D.fill_between` for API documentation. :target: /gallery/mplot3d/fillbetween3d.html :align: center +.. versionadded:: 3.10 + .. _polygon3d: Polygon plots diff --git a/lib/matplotlib/_api/deprecation.py b/lib/matplotlib/_api/deprecation.py index e9722f5d26c4..65a754bbb43d 100644 --- a/lib/matplotlib/_api/deprecation.py +++ b/lib/matplotlib/_api/deprecation.py @@ -26,25 +26,20 @@ def _generate_deprecation_warning( addendum='', *, removal=''): if pending: if removal: - raise ValueError( - "A pending deprecation cannot have a scheduled removal") - else: - if not removal: - macro, meso, *_ = since.split('.') - removal = f'{macro}.{int(meso) + 2}' - removal = f"in {removal}" + raise ValueError("A pending deprecation cannot have a scheduled removal") + elif removal == '': + macro, meso, *_ = since.split('.') + removal = f'{macro}.{int(meso) + 2}' if not message: message = ( - ("The %(name)s %(obj_type)s" if obj_type else "%(name)s") - + (" will be deprecated in a future version" - if pending else - " was deprecated in Matplotlib %(since)s and will be removed %(removal)s" - ) - + "." - + (" Use %(alternative)s instead." if alternative else "") - + (" %(addendum)s" if addendum else "")) - warning_cls = (PendingDeprecationWarning if pending - else MatplotlibDeprecationWarning) + ("The %(name)s %(obj_type)s" if obj_type else "%(name)s") + + (" will be deprecated in a future version" if pending else + (" was deprecated in Matplotlib %(since)s" + + (" and will be removed in %(removal)s" if removal else ""))) + + "." + + (" Use %(alternative)s instead." if alternative else "") + + (" %(addendum)s" if addendum else "")) + warning_cls = PendingDeprecationWarning if pending else MatplotlibDeprecationWarning return warning_cls(message % dict( func=name, name=name, obj_type=obj_type, since=since, removal=removal, alternative=alternative, addendum=addendum)) @@ -295,7 +290,7 @@ def wrapper(*args, **kwargs): warn_deprecated( since, message=f"The {old!r} parameter of {func.__name__}() " f"has been renamed {new!r} since Matplotlib {since}; support " - f"for the old name will be dropped %(removal)s.") + f"for the old name will be dropped in %(removal)s.") kwargs[new] = kwargs.pop(old) return func(*args, **kwargs) @@ -390,12 +385,12 @@ def wrapper(*inner_args, **inner_kwargs): warn_deprecated( since, message=f"Additional positional arguments to " f"{func.__name__}() are deprecated since %(since)s and " - f"support for them will be removed %(removal)s.") + f"support for them will be removed in %(removal)s.") elif is_varkwargs and arguments.get(name): warn_deprecated( since, message=f"Additional keyword arguments to " f"{func.__name__}() are deprecated since %(since)s and " - f"support for them will be removed %(removal)s.") + f"support for them will be removed in %(removal)s.") # We cannot just check `name not in arguments` because the pyplot # wrappers always pass all arguments explicitly. elif any(name in d and d[name] != _deprecated_parameter @@ -453,7 +448,7 @@ def wrapper(*args, **kwargs): warn_deprecated( since, message="Passing the %(name)s %(obj_type)s " "positionally is deprecated since Matplotlib %(since)s; the " - "parameter will become keyword-only %(removal)s.", + "parameter will become keyword-only in %(removal)s.", name=name, obj_type=f"parameter of {func.__name__}()") return func(*args, **kwargs) diff --git a/lib/matplotlib/_api/deprecation.pyi b/lib/matplotlib/_api/deprecation.pyi index d0d04d987410..e050290662d9 100644 --- a/lib/matplotlib/_api/deprecation.pyi +++ b/lib/matplotlib/_api/deprecation.pyi @@ -1,6 +1,6 @@ from collections.abc import Callable import contextlib -from typing import Any, ParamSpec, TypedDict, TypeVar, overload +from typing import Any, Literal, ParamSpec, TypedDict, TypeVar, overload from typing_extensions import ( Unpack, # < Py 3.11 ) @@ -17,7 +17,7 @@ class DeprecationKwargs(TypedDict, total=False): pending: bool obj_type: str addendum: str - removal: str + removal: str | Literal[False] class NamedDeprecationKwargs(DeprecationKwargs, total=False): name: str diff --git a/lib/matplotlib/_c_internal_utils.pyi b/lib/matplotlib/_c_internal_utils.pyi index 3efc81bc8332..ccc172cde27a 100644 --- a/lib/matplotlib/_c_internal_utils.pyi +++ b/lib/matplotlib/_c_internal_utils.pyi @@ -1,4 +1,5 @@ def display_is_valid() -> bool: ... +def xdisplay_is_valid() -> bool: ... def Win32_GetForegroundWindow() -> int | None: ... def Win32_SetForegroundWindow(hwnd: int) -> None: ... diff --git a/lib/matplotlib/_docstring.py b/lib/matplotlib/_docstring.py index 7e9448fd63c8..8cc7d623efe5 100644 --- a/lib/matplotlib/_docstring.py +++ b/lib/matplotlib/_docstring.py @@ -68,12 +68,6 @@ def __call__(self, func): func.__doc__ = inspect.cleandoc(func.__doc__) % self.params return func - def update(self, *args, **kwargs): - """ - Update ``self.params`` (which must be a dict) with the supplied args. - """ - self.params.update(*args, **kwargs) - class _ArtistKwdocLoader(dict): def __missing__(self, key): @@ -89,23 +83,45 @@ def __missing__(self, key): return self.setdefault(key, kwdoc(cls)) -class _ArtistPropertiesSubstitution(Substitution): +class _ArtistPropertiesSubstitution: """ - A `.Substitution` with two additional features: - - - Substitutions of the form ``%(classname:kwdoc)s`` (ending with the - literal ":kwdoc" suffix) trigger lookup of an Artist subclass with the - given *classname*, and are substituted with the `.kwdoc` of that class. - - Decorating a class triggers substitution both on the class docstring and - on the class' ``__init__`` docstring (which is a commonly required - pattern for Artist subclasses). + A class to substitute formatted placeholders in docstrings. + + This is realized in a single instance ``_docstring.interpd``. + + Use `~._ArtistPropertiesSubstition.register` to define placeholders and + their substitution, e.g. ``_docstring.interpd.register(name="some value")``. + + Use this as a decorator to apply the substitution:: + + @_docstring.interpd + def some_func(): + '''Replace %(name)s.''' + + Decorating a class triggers substitution both on the class docstring and + on the class' ``__init__`` docstring (which is a commonly required + pattern for Artist subclasses). + + Substitutions of the form ``%(classname:kwdoc)s`` (ending with the + literal ":kwdoc" suffix) trigger lookup of an Artist subclass with the + given *classname*, and are substituted with the `.kwdoc` of that class. """ def __init__(self): self.params = _ArtistKwdocLoader() + def register(self, **kwargs): + """ + Register substitutions. + + ``_docstring.interpd.register(name="some value")`` makes "name" available + as a named parameter that will be replaced by "some value". + """ + self.params.update(**kwargs) + def __call__(self, obj): - super().__call__(obj) + if obj.__doc__: + obj.__doc__ = inspect.cleandoc(obj.__doc__) % self.params if isinstance(obj, type) and obj.__init__ != object.__init__: self(obj.__init__) return obj diff --git a/lib/matplotlib/_docstring.pyi b/lib/matplotlib/_docstring.pyi index 62cea3da4476..fb52d0846123 100644 --- a/lib/matplotlib/_docstring.pyi +++ b/lib/matplotlib/_docstring.pyi @@ -21,8 +21,9 @@ class _ArtistKwdocLoader(dict[str, str]): def __missing__(self, key: str) -> str: ... -class _ArtistPropertiesSubstitution(Substitution): +class _ArtistPropertiesSubstitution: def __init__(self) -> None: ... + def register(self, **kwargs) -> None: ... def __call__(self, obj: _T) -> _T: ... diff --git a/lib/matplotlib/_enums.py b/lib/matplotlib/_enums.py index c8c50f7c3028..773011d36bf6 100644 --- a/lib/matplotlib/_enums.py +++ b/lib/matplotlib/_enums.py @@ -181,5 +181,7 @@ def demo(): + ", ".join([f"'{cs.name}'" for cs in CapStyle]) \ + "}" -_docstring.interpd.update({'JoinStyle': JoinStyle.input_description, - 'CapStyle': CapStyle.input_description}) +_docstring.interpd.register( + JoinStyle=JoinStyle.input_description, + CapStyle=CapStyle.input_description, +) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 0f6811a3bab9..a0236a6956cb 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -28,13 +28,6 @@ subprocess_creation_flags = ( subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0) -# Other potential writing methods: -# * http://pymedia.org/ -# * libming (produces swf) python wrappers: https://github.com/libming/libming -# * Wrap x264 API: - -# (https://stackoverflow.com/q/2940671/) - def adjusted_figsize(w, h, dpi, n): """ diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index d7b649ae437f..6da5dc179798 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -37,6 +37,7 @@ _AxesBase, _TransformedBoundsLocator, _process_plot_format) from matplotlib.axes._secondary_axes import SecondaryAxis from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer +from matplotlib.transforms import ScaledRotation _log = logging.getLogger(__name__) @@ -86,13 +87,6 @@ class Axes(_AxesBase): methods instead; e.g. from `.pyplot` or `.Figure`: `~.pyplot.subplots`, `~.pyplot.subplot_mosaic` or `.Figure.add_axes`. - Attributes - ---------- - dataLim : `.Bbox` - The bounding box enclosing all data displayed in the Axes. - viewLim : `.Bbox` - The view limits in data coordinates. - """ ### Labelling, legend and texts @@ -761,6 +755,15 @@ def axhline(self, y=0, xmin=0, xmax=1, **kwargs): Returns ------- `~matplotlib.lines.Line2D` + A `.Line2D` specified via two points ``(xmin, y)``, ``(xmax, y)``. + Its transform is set such that *x* is in + :ref:`axes coordinates ` and *y* is in + :ref:`data coordinates `. + + This is still a generic line and the horizontal character is only + realized through using identical *y* values for both points. Thus, + if you want to change the *y* value later, you have to provide two + values ``line.set_ydata([3, 3])``. Other Parameters ---------------- @@ -835,6 +838,15 @@ def axvline(self, x=0, ymin=0, ymax=1, **kwargs): Returns ------- `~matplotlib.lines.Line2D` + A `.Line2D` specified via two points ``(x, ymin)``, ``(x, ymax)``. + Its transform is set such that *x* is in + :ref:`data coordinates ` and *y* is in + :ref:`axes coordinates `. + + This is still a generic line and the vertical character is only + realized through using identical *x* values for both points. Thus, + if you want to change the *x* value later, you have to provide two + values ``line.set_xdata([3, 3])``. Other Parameters ---------------- @@ -3283,9 +3295,9 @@ def pie(self, x, explode=None, labels=None, colors=None, if explode is None: explode = [0] * len(x) if len(x) != len(labels): - raise ValueError("'label' must be of length 'x'") + raise ValueError(f"'labels' must be of length 'x', not {len(labels)}") if len(x) != len(explode): - raise ValueError("'explode' must be of length 'x'") + raise ValueError(f"'explode' must be of length 'x', not {len(explode)}") if colors is None: get_next_color = self._get_patches_for_fill.get_next_color else: @@ -3298,7 +3310,7 @@ def get_next_color(): _api.check_isinstance(Real, radius=radius, startangle=startangle) if radius <= 0: - raise ValueError(f'radius must be a positive number, not {radius}') + raise ValueError(f"'radius' must be a positive number, not {radius}") # Starting theta1 is the start fraction of the circle theta1 = startangle / 360 @@ -3775,15 +3787,17 @@ def apply_mask(arrays, mask): if self.name == 'polar': for axis in caplines: for l in caplines[axis]: - # Rotate caps to be perpendicular to the error bars for theta, r in zip(l.get_xdata(), l.get_ydata()): - rotation = mtransforms.Affine2D().rotate(theta) - if axis == 'y': - rotation.rotate(-np.pi / 2) - ms = mmarkers.MarkerStyle(marker=marker, - transform=rotation) - self.add_line(mlines.Line2D([theta], [r], marker=ms, - **eb_cap_style)) + trans_shift = self.transShift + rotation_transform = ScaledRotation( + theta=theta, + trans_shift=trans_shift, + is_radial=(axis == 'y') + ) + ms = mmarkers.MarkerStyle( + marker=marker, transform=rotation_transform) + self.add_line( + mlines.Line2D([theta], [r], marker=ms, **eb_cap_style)) else: for axis in caplines: for l in caplines[axis]: @@ -6634,6 +6648,15 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, if x.size == 2 and y.size == 2: style = "image" else: + if x.size != nc + 1: + raise ValueError( + f"Length of X ({x.size}) must be one larger than the " + f"number of columns in C ({nc})") + if y.size != nr + 1: + raise ValueError( + f"Length of Y ({y.size}) must be one larger than the " + f"number of rows in C ({nr})" + ) dx = np.diff(x) dy = np.diff(y) if (np.ptp(dx) < 0.01 * abs(dx.mean()) and @@ -6932,7 +6955,7 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, `~matplotlib.patches.Patch` properties. The following properties additionally accept a sequence of values corresponding to the datasets in *x*: - *edgecolors*, *facecolors*, *lines*, *linestyles*, *hatches*. + *edgecolor*, *facecolor*, *linewidth*, *linestyle*, *hatch*. .. versionadded:: 3.10 Allowing sequences of values in above listed Patch properties. diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 20cea0a917e1..8fc9e3e3cf4d 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -551,6 +551,9 @@ class _AxesBase(martist.Artist): _subclass_uses_cla = False + dataLim: mtransforms.Bbox + """The bounding `.Bbox` enclosing all data displayed in the Axes.""" + @property def _axis_map(self): """A mapping of axis names, e.g. 'x', to `Axis` instances.""" @@ -777,8 +780,8 @@ def __repr__(self): if titles: fields += [f"title={titles}"] for name, axis in self._axis_map.items(): - if axis.get_label() and axis.get_label().get_text(): - fields += [f"{name}label={axis.get_label().get_text()!r}"] + if axis.label and axis.label.get_text(): + fields += [f"{name}label={axis.label.get_text()!r}"] return f"<{self.__class__.__name__}: " + ", ".join(fields) + ">" def get_subplotspec(self): @@ -849,6 +852,7 @@ def _unstale_viewLim(self): @property def viewLim(self): + """The view limits as `.Bbox` in data coordinates.""" self._unstale_viewLim() return self._viewLim @@ -1709,7 +1713,8 @@ def set_adjustable(self, adjustable, share=False): ---------- adjustable : {'box', 'datalim'} If 'box', change the physical dimensions of the Axes. - If 'datalim', change the ``x`` or ``y`` data limits. + If 'datalim', change the ``x`` or ``y`` data limits. This + may ignore explicitly defined axis limits. share : bool, default: False If ``True``, apply the settings to all shared Axes. @@ -2026,11 +2031,17 @@ def apply_aspect(self, position=None): yc = 0.5 * (ymin + ymax) y0 = yc - Ysize / 2.0 y1 = yc + Ysize / 2.0 + if not self.get_autoscaley_on(): + _log.warning("Ignoring fixed y limits to fulfill fixed data aspect " + "with adjustable data limits.") self.set_ybound(y_trf.inverted().transform([y0, y1])) else: xc = 0.5 * (xmin + xmax) x0 = xc - Xsize / 2.0 x1 = xc + Xsize / 2.0 + if not self.get_autoscalex_on(): + _log.warning("Ignoring fixed x limits to fulfill fixed data aspect " + "with adjustable data limits.") self.set_xbound(x_trf.inverted().transform([x0, x1])) def axis(self, arg=None, /, *, emit=True, **kwargs): @@ -2247,8 +2258,8 @@ def add_artist(self, a): Use `add_artist` only for artists for which there is no dedicated "add" method; and if necessary, use a method such as `update_datalim` - to manually update the dataLim if the artist is to be included in - autoscaling. + to manually update the `~.Axes.dataLim` if the artist is to be included + in autoscaling. If no ``transform`` has been specified when creating the artist (e.g. ``artist.get_transform() == None``) then the transform is set to @@ -2265,7 +2276,7 @@ def add_artist(self, a): def add_child_axes(self, ax): """ - Add an `.AxesBase` to the Axes' children; return the child Axes. + Add an `.Axes` to the Axes' children; return the child Axes. This is the lowlevel version. See `.axes.Axes.inset_axes`. """ @@ -2361,7 +2372,7 @@ def _add_text(self, txt): def _update_line_limits(self, line): """ - Figures out the data limit of the given line, updating self.dataLim. + Figures out the data limit of the given line, updating `.Axes.dataLim`. """ path = line.get_path() if path.vertices.size == 0: @@ -3517,7 +3528,7 @@ def get_xlabel(self): """ Get the xlabel text string. """ - label = self.xaxis.get_label() + label = self.xaxis.label return label.get_text() def set_xlabel(self, xlabel, fontdict=None, labelpad=None, *, @@ -3770,7 +3781,7 @@ def get_ylabel(self): """ Get the ylabel text string. """ - label = self.yaxis.get_label() + label = self.yaxis.label return label.get_text() def set_ylabel(self, ylabel, fontdict=None, labelpad=None, *, diff --git a/lib/matplotlib/axes/_secondary_axes.py b/lib/matplotlib/axes/_secondary_axes.py index b01acc4b127d..15a1970fa4a6 100644 --- a/lib/matplotlib/axes/_secondary_axes.py +++ b/lib/matplotlib/axes/_secondary_axes.py @@ -319,4 +319,4 @@ def set_color(self, color): **kwargs : `~matplotlib.axes.Axes` properties. Other miscellaneous Axes parameters. ''' -_docstring.interpd.update(_secax_docstring=_secax_docstring) +_docstring.interpd.register(_secax_docstring=_secax_docstring) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index cbf7928ec44e..8e612bd8c702 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -538,17 +538,19 @@ def __get__(self, instance, owner): # instance._get_tick() can itself try to access the majorTicks # attribute (e.g. in certain projection classes which override # e.g. get_xaxis_text1_transform). In order to avoid infinite - # recursion, first set the majorTicks on the instance to an empty - # list, then create the tick and append it. + # recursion, first set the majorTicks on the instance temporarily + # to an empty lis. Then create the tick; note that _get_tick() + # may call reset_ticks(). Therefore, the final tick list is + # created and assigned afterwards. if self._major: instance.majorTicks = [] tick = instance._get_tick(major=True) - instance.majorTicks.append(tick) + instance.majorTicks = [tick] return instance.majorTicks else: instance.minorTicks = [] tick = instance._get_tick(major=False) - instance.minorTicks.append(tick) + instance.minorTicks = [tick] return instance.minorTicks @@ -572,7 +574,7 @@ class Axis(martist.Artist): The axis label. labelpad : float The distance between the axis label and the tick labels. - Defaults to :rc:`axes.labelpad` = 4. + Defaults to :rc:`axes.labelpad`. offsetText : `~matplotlib.text.Text` A `.Text` object containing the data offset of the ticks (if any). pickradius : float @@ -637,7 +639,8 @@ def __init__(self, axes, *, pickradius=15, clear=True): fontsize=mpl.rcParams['axes.labelsize'], fontweight=mpl.rcParams['axes.labelweight'], color=mpl.rcParams['axes.labelcolor'], - ) + ) #: The `.Text` object of the axis label. + self._set_artist_props(self.label) self.offsetText = mtext.Text(np.nan, np.nan) self._set_artist_props(self.offsetText) @@ -1421,8 +1424,21 @@ def get_gridlines(self): return cbook.silent_list('Line2D gridline', [tick.gridline for tick in ticks]) + def set_label(self, s): + """Assigning legend labels is not supported. Raises RuntimeError.""" + raise RuntimeError( + "A legend label cannot be assigned to an Axis. Did you mean to " + "set the axis label via set_label_text()?") + def get_label(self): - """Return the axis label as a Text instance.""" + """ + Return the axis label as a Text instance. + + .. admonition:: Discouraged + + This overrides `.Artist.get_label`, which is for legend labels, with a new + semantic. It is recommended to use the attribute ``Axis.label`` instead. + """ return self.label def get_offset_text(self): diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index a8170ce4f6b0..95ed49612b35 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1178,26 +1178,12 @@ class Event: def __init__(self, name, canvas, guiEvent=None): self.name = name self.canvas = canvas - self._guiEvent = guiEvent - self._guiEvent_deleted = False + self.guiEvent = guiEvent def _process(self): """Process this event on ``self.canvas``, then unset ``guiEvent``.""" self.canvas.callbacks.process(self.name, self) - self._guiEvent_deleted = True - - @property - def guiEvent(self): - # After deprecation elapses: remove _guiEvent_deleted; make guiEvent a plain - # attribute set to None by _process. - if self._guiEvent_deleted: - _api.warn_deprecated( - "3.8", message="Accessing guiEvent outside of the original GUI event " - "handler is unsafe and deprecated since %(since)s; in the future, the " - "attribute will be set to None after quitting the event handler. You " - "may separately record the value of the guiEvent attribute at your own " - "risk.") - return self._guiEvent + self.guiEvent = None class DrawEvent(Event): @@ -2097,12 +2083,6 @@ def print_figure( if dpi == 'figure': dpi = getattr(self.figure, '_original_dpi', self.figure.dpi) - if kwargs.get("papertype") == 'auto': - # When deprecation elapses, remove backend_ps._get_papertype & its callers. - _api.warn_deprecated( - "3.8", name="papertype='auto'", addendum="Pass an explicit paper type, " - "'figure', or omit the *papertype* argument entirely.") - # Remove the figure manager, if any, to avoid resizing the GUI widget. with (cbook._setattr_cm(self, manager=None), self._switch_canvas_and_return_print_method(format, backend) @@ -2207,20 +2187,6 @@ def get_default_filename(self): default_filetype = self.get_default_filetype() return f'{default_basename}.{default_filetype}' - @_api.deprecated("3.8") - def switch_backends(self, FigureCanvasClass): - """ - Instantiate an instance of FigureCanvasClass - - This is used for backend switching, e.g., to instantiate a - FigureCanvasPS from a FigureCanvasGTK. Note, deep copying is - not done, so any changes to one of the instances (e.g., setting - figure size or line props), will be reflected in the other - """ - newCanvas = FigureCanvasClass(self.figure) - newCanvas._is_saving = self._is_saving - return newCanvas - def mpl_connect(self, s, func): """ Bind function *func* to event *s*. @@ -3333,7 +3299,7 @@ def _get_image_filename(self, tool): _api.warn_deprecated( "3.9", message=f"Loading icon {tool.image!r} from the current " "directory or from Matplotlib's image directory. This behavior " - "is deprecated since %(since)s and will be removed %(removal)s; " + "is deprecated since %(since)s and will be removed in %(removal)s; " "Tool.image should be set to a path relative to the Tool's source " "file, or to an absolute path.") return os.path.abspath(fname) diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index c2fc61e386d8..70be504666fc 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -199,13 +199,11 @@ class TimerBase: class Event: name: str canvas: FigureCanvasBase + guiEvent: Any def __init__( self, name: str, canvas: FigureCanvasBase, guiEvent: Any | None = ... ) -> None: ... - @property - def guiEvent(self) -> Any: ... - class DrawEvent(Event): renderer: RendererBase def __init__( @@ -348,7 +346,6 @@ class FigureCanvasBase: def get_default_filetype(cls) -> str: ... def get_default_filename(self) -> str: ... _T = TypeVar("_T", bound=FigureCanvasBase) - def switch_backends(self, FigureCanvasClass: type[_T]) -> _T: ... def mpl_connect(self, s: str, func: Callable[[Event], Any]) -> int: ... def mpl_disconnect(self, cid: int) -> None: ... def new_timer( diff --git a/lib/matplotlib/backend_tools.pyi b/lib/matplotlib/backend_tools.pyi index 446f713292e1..f86a207c7545 100644 --- a/lib/matplotlib/backend_tools.pyi +++ b/lib/matplotlib/backend_tools.pyi @@ -75,8 +75,8 @@ class ToolXScale(AxisScaleBase): def set_scale(self, ax, scale: str | ScaleBase) -> None: ... class ToolViewsPositions(ToolBase): - views: dict[Figure | Axes, cbook.Stack] - positions: dict[Figure | Axes, cbook.Stack] + views: dict[Figure | Axes, cbook._Stack] + positions: dict[Figure | Axes, cbook._Stack] home_views: dict[Figure, dict[Axes, tuple[float, float, float, float]]] def add_figure(self, figure: Figure) -> None: ... def clear(self, figure: Figure) -> None: ... diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index df06440a9826..d7c2f59be6b9 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -176,8 +176,7 @@ def __init__(self, figure=None, master=None): self._tkcanvas_image_region = self._tkcanvas.create_image( w//2, h//2, image=self._tkphoto) self._tkcanvas.bind("", self.resize) - if sys.platform == 'win32': - self._tkcanvas.bind("", self._update_device_pixel_ratio) + self._tkcanvas.bind("", self._update_device_pixel_ratio) self._tkcanvas.bind("", self.key_press) self._tkcanvas.bind("", self.motion_notify_event) self._tkcanvas.bind("", self.enter_notify_event) @@ -234,11 +233,15 @@ def filter_destroy(event): self._rubberband_rect_white = None def _update_device_pixel_ratio(self, event=None): - # Tk gives scaling with respect to 72 DPI, but Windows screens are - # scaled vs 96 dpi, and pixel ratio settings are given in whole - # percentages, so round to 2 digits. - ratio = round(self._tkcanvas.tk.call('tk', 'scaling') / (96 / 72), 2) - if self._set_device_pixel_ratio(ratio): + ratio = None + if sys.platform == 'win32': + # Tk gives scaling with respect to 72 DPI, but Windows screens are + # scaled vs 96 dpi, and pixel ratio settings are given in whole + # percentages, so round to 2 digits. + ratio = round(self._tkcanvas.tk.call('tk', 'scaling') / (96 / 72), 2) + elif sys.platform == "linux": + ratio = self._tkcanvas.winfo_fpixels('1i') / 96 + if ratio is not None and self._set_device_pixel_ratio(ratio): # The easiest way to resize the canvas is to resize the canvas # widget itself, since we implement all the logic for resizing the # canvas backing store on that event. diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index 92253c02c1b5..ae361f0cceb4 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -266,10 +266,6 @@ def buffer_rgba(self): def tostring_argb(self): return np.asarray(self._renderer).take([3, 0, 1, 2], axis=2).tobytes() - @_api.deprecated("3.8", alternative="buffer_rgba") - def tostring_rgb(self): - return np.asarray(self._renderer).take([0, 1, 2], axis=2).tobytes() - def clear(self): self._renderer.clear() @@ -398,16 +394,6 @@ def get_renderer(self): self._lastKey = key return self.renderer - @_api.deprecated("3.8", alternative="buffer_rgba") - def tostring_rgb(self): - """ - Get the image as RGB `bytes`. - - `draw` must be called at least once before this function will work and - to update the renderer for any subsequent changes to the Figure. - """ - return self.renderer.tostring_rgb() - def tostring_argb(self): """ Get the image as ARGB `bytes`. diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 7e3e09f034f5..9c542e2a8f8e 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -2663,9 +2663,9 @@ class PdfPages: confusion when using `~.pyplot.savefig` and forgetting the format argument. """ - _UNSET = object() - - def __init__(self, filename, keep_empty=_UNSET, metadata=None): + @_api.delete_parameter("3.10", "keep_empty", + addendum="This parameter does nothing.") + def __init__(self, filename, keep_empty=None, metadata=None): """ Create a new PdfPages object. @@ -2676,10 +2676,6 @@ def __init__(self, filename, keep_empty=_UNSET, metadata=None): The file is opened when a figure is saved for the first time (overwriting any older file with the same name). - keep_empty : bool, optional - If set to False, then empty pdf files will be deleted automatically - when closed. - metadata : dict, optional Information dictionary object (see PDF reference section 10.2.1 'Document Information Dictionary'), e.g.: @@ -2693,13 +2689,6 @@ def __init__(self, filename, keep_empty=_UNSET, metadata=None): self._filename = filename self._metadata = metadata self._file = None - if keep_empty and keep_empty is not self._UNSET: - _api.warn_deprecated("3.8", message=( - "Keeping empty pdf files is deprecated since %(since)s and support " - "will be removed %(removal)s.")) - self._keep_empty = keep_empty - - keep_empty = _api.deprecate_privatize_attribute("3.8") def __enter__(self): return self @@ -2721,11 +2710,6 @@ def close(self): self._file.finalize() self._file.close() self._file = None - elif self._keep_empty: # True *or* UNSET. - _api.warn_deprecated("3.8", message=( - "Keeping empty pdf files is deprecated since %(since)s and support " - "will be removed %(removal)s.")) - PdfFile(self._filename, metadata=self._metadata).close() # touch the file. def infodict(self): """ diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index daefdb0640ca..48b6e8ac152c 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -14,7 +14,7 @@ from PIL import Image import matplotlib as mpl -from matplotlib import _api, cbook, font_manager as fm +from matplotlib import cbook, font_manager as fm from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, RendererBase ) @@ -898,9 +898,7 @@ class PdfPages: ... pdf.savefig() """ - _UNSET = object() - - def __init__(self, filename, *, keep_empty=_UNSET, metadata=None): + def __init__(self, filename, *, metadata=None): """ Create a new PdfPages object. @@ -910,10 +908,6 @@ def __init__(self, filename, *, keep_empty=_UNSET, metadata=None): Plots using `PdfPages.savefig` will be written to a file at this location. Any older file with the same name is overwritten. - keep_empty : bool, default: True - If set to False, then empty pdf files will be deleted automatically - when closed. - metadata : dict, optional Information dictionary object (see PDF reference section 10.2.1 'Document Information Dictionary'), e.g.: @@ -929,17 +923,10 @@ def __init__(self, filename, *, keep_empty=_UNSET, metadata=None): """ self._output_name = filename self._n_figures = 0 - if keep_empty and keep_empty is not self._UNSET: - _api.warn_deprecated("3.8", message=( - "Keeping empty pdf files is deprecated since %(since)s and support " - "will be removed %(removal)s.")) - self._keep_empty = keep_empty self._metadata = (metadata or {}).copy() self._info_dict = _create_pdf_info_dict('pgf', self._metadata) self._file = BytesIO() - keep_empty = _api.deprecate_privatize_attribute("3.8") - def _write_header(self, width_inches, height_inches): pdfinfo = ','.join( _metadata_to_str(k, v) for k, v in self._info_dict.items()) @@ -969,11 +956,6 @@ def close(self): self._file.write(rb'\end{document}\n') if self._n_figures > 0: self._run_latex() - elif self._keep_empty: - _api.warn_deprecated("3.8", message=( - "Keeping empty pdf files is deprecated since %(since)s and support " - "will be removed %(removal)s.")) - open(self._output_name, 'wb').close() self._file.close() def _run_latex(self): diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 5f224f38af1e..4f4c27cce955 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -39,12 +39,6 @@ debugPS = False -@_api.caching_module_getattr -class __getattr__: - # module-level deprecations - psDefs = _api.deprecated("3.8", obj_type="")(property(lambda self: _psDefs)) - - papersize = {'letter': (8.5, 11), 'legal': (8.5, 14), 'ledger': (11, 17), @@ -72,15 +66,6 @@ class __getattr__: 'b10': (1.26, 1.76)} -def _get_papertype(w, h): - for key, (pw, ph) in sorted(papersize.items(), reverse=True): - if key.startswith('l'): - continue - if w < pw and h < ph: - return key - return 'a0' - - def _nums_to_str(*args, sep=" "): return sep.join(f"{arg:1.3f}".rstrip("0").rstrip(".") for arg in args) @@ -828,7 +813,7 @@ def _print_ps( if papertype is None: papertype = mpl.rcParams['ps.papersize'] papertype = papertype.lower() - _api.check_in_list(['figure', 'auto', *papersize], papertype=papertype) + _api.check_in_list(['figure', *papersize], papertype=papertype) orientation = _api.check_getitem( _Orientation, orientation=orientation.lower()) @@ -858,9 +843,6 @@ def _print_figure( # find the appropriate papertype width, height = self.figure.get_size_inches() - if papertype == 'auto': - papertype = _get_papertype(*orientation.swap_if_landscape((width, height))) - if is_eps or papertype == 'figure': paper_width, paper_height = width, height else: @@ -1041,8 +1023,6 @@ def _print_figure_tex( paper_width, paper_height = orientation.swap_if_landscape( self.figure.get_size_inches()) else: - if papertype == 'auto': - papertype = _get_papertype(width, height) paper_width, paper_height = papersize[papertype] psfrag_rotated = _convert_psfrags( diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index e693811df4f0..bc37a15c7a67 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -658,9 +658,6 @@ def set_window_title(self, title): class NavigationToolbar2QT(NavigationToolbar2, QtWidgets.QToolBar): - _message = QtCore.Signal(str) # Remove once deprecation below elapses. - message = _api.deprecate_privatize_attribute("3.8") - toolitems = [*NavigationToolbar2.toolitems] toolitems.insert( # Add 'customize' action after 'subplots' @@ -783,7 +780,6 @@ def zoom(self, *args): self._update_buttons_checked() def set_message(self, s): - self._message.emit(s) if self.coordinates: self.locLabel.setText(s) diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index 529f45829999..b025ef3e056e 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -54,7 +54,7 @@ def convert_limits(lim, converter): (None, f"{name.title()}-Axis"), ('Min', axis_limits[name][0]), ('Max', axis_limits[name][1]), - ('Label', axis.get_label().get_text()), + ('Label', axis.label.get_text()), ('Scale', [axis.get_scale(), 'linear', 'log', 'symlog', 'logit']), sep, diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 2411784af3ec..71ff5a941601 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -32,6 +32,29 @@ from matplotlib import _api, _c_internal_utils +class _ExceptionInfo: + """ + A class to carry exception information around. + + This is used to store and later raise exceptions. It's an alternative to + directly storing Exception instances that circumvents traceback-related + issues: caching tracebacks can keep user's objects in local namespaces + alive indefinitely, which can lead to very surprising memory issues for + users and result in incorrect tracebacks. + """ + + def __init__(self, cls, *args): + self._cls = cls + self._args = args + + @classmethod + def from_exception(cls, exc): + return cls(type(exc), *exc.args) + + def to_exception(self): + return self._cls(*self._args) + + def _get_running_interactive_framework(): """ Return the interactive framework whose event loop is currently running, if @@ -72,7 +95,7 @@ def _get_running_interactive_framework(): if frame.f_code in codes: return "tk" frame = frame.f_back - # premetively break reference cycle between locals and the frame + # Preemptively break reference cycle between locals and the frame. del frame macosx = sys.modules.get("matplotlib.backends._macosx") if macosx and macosx.event_loop_is_running(): @@ -117,6 +140,61 @@ def _weak_or_strong_ref(func, callback): return _StrongRef(func) +class _UnhashDict: + """ + A minimal dict-like class that also supports unhashable keys, storing them + in a list of key-value pairs. + + This class only implements the interface needed for `CallbackRegistry`, and + tries to minimize the overhead for the hashable case. + """ + + def __init__(self, pairs): + self._dict = {} + self._pairs = [] + for k, v in pairs: + self[k] = v + + def __setitem__(self, key, value): + try: + self._dict[key] = value + except TypeError: + for i, (k, v) in enumerate(self._pairs): + if k == key: + self._pairs[i] = (key, value) + break + else: + self._pairs.append((key, value)) + + def __getitem__(self, key): + try: + return self._dict[key] + except TypeError: + pass + for k, v in self._pairs: + if k == key: + return v + raise KeyError(key) + + def pop(self, key, *args): + try: + if key in self._dict: + return self._dict.pop(key) + except TypeError: + for i, (k, v) in enumerate(self._pairs): + if k == key: + del self._pairs[i] + return v + if args: + return args[0] + raise KeyError(key) + + def __iter__(self): + yield from self._dict + for k, v in self._pairs: + yield k + + class CallbackRegistry: """ Handle registering, processing, blocking, and disconnecting @@ -176,14 +254,14 @@ class CallbackRegistry: # We maintain two mappings: # callbacks: signal -> {cid -> weakref-to-callback} - # _func_cid_map: signal -> {weakref-to-callback -> cid} + # _func_cid_map: {(signal, weakref-to-callback) -> cid} def __init__(self, exception_handler=_exception_printer, *, signals=None): self._signals = None if signals is None else list(signals) # Copy it. self.exception_handler = exception_handler self.callbacks = {} self._cid_gen = itertools.count() - self._func_cid_map = {} + self._func_cid_map = _UnhashDict([]) # A hidden variable that marks cids that need to be pickled. self._pickled_cids = set() @@ -204,27 +282,25 @@ def __setstate__(self, state): cid_count = state.pop('_cid_gen') vars(self).update(state) self.callbacks = { - s: {cid: _weak_or_strong_ref(func, self._remove_proxy) + s: {cid: _weak_or_strong_ref(func, functools.partial(self._remove_proxy, s)) for cid, func in d.items()} for s, d in self.callbacks.items()} - self._func_cid_map = { - s: {proxy: cid for cid, proxy in d.items()} - for s, d in self.callbacks.items()} + self._func_cid_map = _UnhashDict( + ((s, proxy), cid) + for s, d in self.callbacks.items() for cid, proxy in d.items()) self._cid_gen = itertools.count(cid_count) def connect(self, signal, func): """Register *func* to be called when signal *signal* is generated.""" if self._signals is not None: _api.check_in_list(self._signals, signal=signal) - self._func_cid_map.setdefault(signal, {}) - proxy = _weak_or_strong_ref(func, self._remove_proxy) - if proxy in self._func_cid_map[signal]: - return self._func_cid_map[signal][proxy] - cid = next(self._cid_gen) - self._func_cid_map[signal][proxy] = cid - self.callbacks.setdefault(signal, {}) - self.callbacks[signal][cid] = proxy - return cid + proxy = _weak_or_strong_ref(func, functools.partial(self._remove_proxy, signal)) + try: + return self._func_cid_map[signal, proxy] + except KeyError: + cid = self._func_cid_map[signal, proxy] = next(self._cid_gen) + self.callbacks.setdefault(signal, {})[cid] = proxy + return cid def _connect_picklable(self, signal, func): """ @@ -238,23 +314,18 @@ def _connect_picklable(self, signal, func): # Keep a reference to sys.is_finalizing, as sys may have been cleared out # at that point. - def _remove_proxy(self, proxy, *, _is_finalizing=sys.is_finalizing): + def _remove_proxy(self, signal, proxy, *, _is_finalizing=sys.is_finalizing): if _is_finalizing(): # Weakrefs can't be properly torn down at that point anymore. return - for signal, proxy_to_cid in list(self._func_cid_map.items()): - cid = proxy_to_cid.pop(proxy, None) - if cid is not None: - del self.callbacks[signal][cid] - self._pickled_cids.discard(cid) - break - else: - # Not found + cid = self._func_cid_map.pop((signal, proxy), None) + if cid is not None: + del self.callbacks[signal][cid] + self._pickled_cids.discard(cid) + else: # Not found return - # Clean up empty dicts - if len(self.callbacks[signal]) == 0: + if len(self.callbacks[signal]) == 0: # Clean up empty dicts del self.callbacks[signal] - del self._func_cid_map[signal] def disconnect(self, cid): """ @@ -263,24 +334,16 @@ def disconnect(self, cid): No error is raised if such a callback does not exist. """ self._pickled_cids.discard(cid) - # Clean up callbacks - for signal, cid_to_proxy in list(self.callbacks.items()): - proxy = cid_to_proxy.pop(cid, None) - if proxy is not None: + for signal, proxy in self._func_cid_map: + if self._func_cid_map[signal, proxy] == cid: break - else: - # Not found + else: # Not found return - - proxy_to_cid = self._func_cid_map[signal] - for current_proxy, current_cid in list(proxy_to_cid.items()): - if current_cid == cid: - assert proxy is current_proxy - del proxy_to_cid[current_proxy] - # Clean up empty dicts - if len(self.callbacks[signal]) == 0: + assert self.callbacks[signal][cid] == proxy + del self.callbacks[signal][cid] + self._func_cid_map.pop((signal, proxy)) + if len(self.callbacks[signal]) == 0: # Clean up empty dicts del self.callbacks[signal] - del self._func_cid_map[signal] def process(self, s, *args, **kwargs): """ @@ -503,9 +566,7 @@ def is_scalar_or_string(val): return isinstance(val, str) or not np.iterable(val) -@_api.delete_parameter( - "3.8", "np_load", alternative="open(get_sample_data(..., asfileobj=False))") -def get_sample_data(fname, asfileobj=True, *, np_load=True): +def get_sample_data(fname, asfileobj=True): """ Return a sample data file. *fname* is a path relative to the :file:`mpl-data/sample_data` directory. If *asfileobj* is `True` @@ -524,10 +585,7 @@ def get_sample_data(fname, asfileobj=True, *, np_load=True): if suffix == '.gz': return gzip.open(path) elif suffix in ['.npy', '.npz']: - if np_load: - return np.load(path) - else: - return path.open('rb') + return np.load(path) elif suffix in ['.csv', '.xrc', '.txt']: return path.open('r') else: @@ -567,113 +625,6 @@ def flatten(seq, scalarp=is_scalar_or_string): yield from flatten(item, scalarp) -@_api.deprecated("3.8") -class Stack: - """ - Stack of elements with a movable cursor. - - Mimics home/back/forward in a web browser. - """ - - def __init__(self, default=None): - self.clear() - self._default = default - - def __call__(self): - """Return the current element, or None.""" - if not self._elements: - return self._default - else: - return self._elements[self._pos] - - def __len__(self): - return len(self._elements) - - def __getitem__(self, ind): - return self._elements[ind] - - def forward(self): - """Move the position forward and return the current element.""" - self._pos = min(self._pos + 1, len(self._elements) - 1) - return self() - - def back(self): - """Move the position back and return the current element.""" - if self._pos > 0: - self._pos -= 1 - return self() - - def push(self, o): - """ - Push *o* to the stack at current position. Discard all later elements. - - *o* is returned. - """ - self._elements = self._elements[:self._pos + 1] + [o] - self._pos = len(self._elements) - 1 - return self() - - def home(self): - """ - Push the first element onto the top of the stack. - - The first element is returned. - """ - if not self._elements: - return - self.push(self._elements[0]) - return self() - - def empty(self): - """Return whether the stack is empty.""" - return len(self._elements) == 0 - - def clear(self): - """Empty the stack.""" - self._pos = -1 - self._elements = [] - - def bubble(self, o): - """ - Raise all references of *o* to the top of the stack, and return it. - - Raises - ------ - ValueError - If *o* is not in the stack. - """ - if o not in self._elements: - raise ValueError('Given element not contained in the stack') - old_elements = self._elements.copy() - self.clear() - top_elements = [] - for elem in old_elements: - if elem == o: - top_elements.append(elem) - else: - self.push(elem) - for _ in top_elements: - self.push(o) - return o - - def remove(self, o): - """ - Remove *o* from the stack. - - Raises - ------ - ValueError - If *o* is not in the stack. - """ - if o not in self._elements: - raise ValueError('Given element not contained in the stack') - old_elements = self._elements.copy() - self.clear() - for elem in old_elements: - if elem != o: - self.push(elem) - - class _Stack: """ Stack of elements with a movable cursor. @@ -873,10 +824,6 @@ def __setstate__(self, state): def __contains__(self, item): return item in self._mapping - @_api.deprecated("3.8", alternative="none, you no longer need to clean a Grouper") - def clean(self): - """Clean dead weak references from the dictionary.""" - def join(self, a, *args): """ Join given arguments into the same set. Accepts one or more arguments. diff --git a/lib/matplotlib/cbook.pyi b/lib/matplotlib/cbook.pyi index d727b8065b7a..cc6b4e8f4e19 100644 --- a/lib/matplotlib/cbook.pyi +++ b/lib/matplotlib/cbook.pyi @@ -74,26 +74,18 @@ def open_file_cm( def is_scalar_or_string(val: Any) -> bool: ... @overload def get_sample_data( - fname: str | os.PathLike, asfileobj: Literal[True] = ..., *, np_load: Literal[True] -) -> np.ndarray: ... + fname: str | os.PathLike, asfileobj: Literal[True] = ... +) -> np.ndarray | IO: ... @overload -def get_sample_data( - fname: str | os.PathLike, - asfileobj: Literal[True] = ..., - *, - np_load: Literal[False] = ..., -) -> IO: ... -@overload -def get_sample_data( - fname: str | os.PathLike, asfileobj: Literal[False], *, np_load: bool = ... -) -> str: ... +def get_sample_data(fname: str | os.PathLike, asfileobj: Literal[False]) -> str: ... def _get_data_path(*args: Path | str) -> Path: ... def flatten( seq: Iterable[Any], scalarp: Callable[[Any], bool] = ... ) -> Generator[Any, None, None]: ... -class Stack(Generic[_T]): - def __init__(self, default: _T | None = ...) -> None: ... +class _Stack(Generic[_T]): + def __init__(self) -> None: ... + def clear(self) -> None: ... def __call__(self) -> _T: ... def __len__(self) -> int: ... def __getitem__(self, ind: int) -> _T: ... @@ -101,10 +93,6 @@ class Stack(Generic[_T]): def back(self) -> _T: ... def push(self, o: _T) -> _T: ... def home(self) -> _T: ... - def empty(self) -> bool: ... - def clear(self) -> None: ... - def bubble(self, o: _T) -> _T: ... - def remove(self, o: _T) -> None: ... def safe_masked_invalid(x: ArrayLike, copy: bool = ...) -> np.ndarray: ... def print_cycles( @@ -114,7 +102,6 @@ def print_cycles( class Grouper(Generic[_T]): def __init__(self, init: Iterable[_T] = ...) -> None: ... def __contains__(self, item: _T) -> bool: ... - def clean(self) -> None: ... def join(self, a: _T, *args: _T) -> None: ... def joined(self, a: _T, b: _T) -> bool: ... def remove(self, a: _T) -> None: ... diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index 025cb84db1d7..27333f8dba8a 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -651,7 +651,7 @@ def _format_cursor_data_override(self, data): # The docstrings here must be generic enough to apply to all relevant methods. -mpl._docstring.interpd.update( +mpl._docstring.interpd.register( cmap_doc="""\ cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` The Colormap instance or registered colormap name used to map scalar data diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index ef333d396101..397e4c12557d 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -113,10 +113,10 @@ def __init__(self, *, where *onoffseq* is an even length tuple of on and off ink lengths in points. For examples, see :doc:`/gallery/lines_bars_and_markers/linestyles`. - capstyle : `.CapStyle`-like, default: :rc:`patch.capstyle` + capstyle : `.CapStyle`-like, default: 'butt' Style to use for capping lines for all paths in the collection. Allowed values are %(CapStyle)s. - joinstyle : `.JoinStyle`-like, default: :rc:`patch.joinstyle` + joinstyle : `.JoinStyle`-like, default: 'round' Style to use for joining lines for all paths in the collection. Allowed values are %(JoinStyle)s. antialiaseds : bool or list of bool, default: :rc:`patch.antialiased` diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 296f072a4af1..89e511fa1428 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -26,7 +26,7 @@ _log = logging.getLogger(__name__) -_docstring.interpd.update( +_docstring.interpd.register( _make_axes_kw_doc=""" location : None or {'left', 'right', 'top', 'bottom'} The location, relative to the parent Axes, where the colorbar Axes @@ -86,11 +86,6 @@ If *False* the minimum and maximum colorbar extensions will be triangular (the default). If *True* the extensions will be rectangular. -spacing : {'uniform', 'proportional'} - For discrete colorbars (`.BoundaryNorm` or contours), 'uniform' gives each - color the same space; 'proportional' makes the space proportional to the - data interval. - ticks : None or list of ticks or Locator If None, ticks are determined automatically from the input. @@ -109,9 +104,15 @@ If unset, the colormap will be displayed on a 0-1 scale. If sequences, *values* must have a length 1 less than *boundaries*. For each region delimited by adjacent entries in *boundaries*, the color mapped - to the corresponding value in values will be used. + to the corresponding value in *values* will be used. The size of each + region is determined by the *spacing* parameter. Normally only useful for indexed colors (i.e. ``norm=NoNorm()``) or other - unusual circumstances.""") + unusual circumstances. + +spacing : {'uniform', 'proportional'} + For discrete colorbars (`.BoundaryNorm` or contours), 'uniform' gives each + color the same space; 'proportional' makes the space proportional to the + data interval.""") def _set_ticks_on_axis_warn(*args, **kwargs): diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 2dbbb51f6196..55090252d24e 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -3097,8 +3097,22 @@ def rgb_to_hsv(arr): dtype=np.promote_types(arr.dtype, np.float32), # Don't work on ints. ndmin=2, # In case input was 1D. ) + out = np.zeros_like(arr) arr_max = arr.max(-1) + # Check if input is in the expected range + if np.any(arr_max > 1): + raise ValueError( + "Input array must be in the range [0, 1]. " + f"Found a maximum value of {arr_max.max()}" + ) + + if arr.min() < 0: + raise ValueError( + "Input array must be in the range [0, 1]. " + f"Found a minimum value of {arr.min()}" + ) + ipos = arr_max > 0 delta = np.ptp(arr, -1) s = np.zeros_like(delta) diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index ff115b10e6d8..05fbedef2c68 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -536,7 +536,7 @@ def _find_closest_point_on_path(xys, p): return (d2s[imin], projs[imin], (imin, imin+1)) -_docstring.interpd.update(contour_set_attributes=r""" +_docstring.interpd.register(contour_set_attributes=r""" Attributes ---------- ax : `~matplotlib.axes.Axes` @@ -1450,7 +1450,7 @@ def _initialize_x_y(self, z): return np.meshgrid(x, y) -_docstring.interpd.update(contour_doc=""" +_docstring.interpd.register(contour_doc=""" `.contour` and `.contourf` draw contour lines and filled contours, respectively. Except as noted, function signatures and return values are the same for both versions. @@ -1643,7 +1643,7 @@ def _initialize_x_y(self, z): specifies the line style for negative contours. If *negative_linestyles* is *None*, the default is taken from - :rc:`contour.negative_linestyles`. + :rc:`contour.negative_linestyle`. *negative_linestyles* can also be an iterable of the above strings specifying a set of linestyles to be used. If this iterable is shorter than diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 15de61f69df7..511e1c6df6cc 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -37,7 +37,7 @@ is achievable for (approximately) 70 years on either side of the epoch, and 20 microseconds for the rest of the allowable range of dates (year 0001 to 9999). The epoch can be changed at import time via `.dates.set_epoch` or -:rc:`dates.epoch` to other dates if necessary; see +:rc:`date.epoch` to other dates if necessary; see :doc:`/gallery/ticks/date_precision_and_epochs` for a discussion. .. note:: @@ -267,7 +267,7 @@ def set_epoch(epoch): """ Set the epoch (origin for dates) for datetime calculations. - The default epoch is :rc:`dates.epoch` (by default 1970-01-01T00:00). + The default epoch is :rc:`date.epoch`. If microsecond accuracy is desired, the date being plotted needs to be within approximately 70 years of the epoch. Matplotlib internally diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 040ca5ef4365..bd21367ce73d 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -340,7 +340,7 @@ def _read(self): byte = self.file.read(1)[0] self._dtable[byte](self, byte) if self._missing_font: - raise self._missing_font + raise self._missing_font.to_exception() name = self._dtable[byte].__name__ if name == "_push": down_stack.append(down_stack[-1]) @@ -368,14 +368,14 @@ def _read_arg(self, nbytes, signed=False): @_dispatch(min=0, max=127, state=_dvistate.inpage) def _set_char_immediate(self, char): self._put_char_real(char) - if isinstance(self.fonts[self.f], FileNotFoundError): + if isinstance(self.fonts[self.f], cbook._ExceptionInfo): return self.h += self.fonts[self.f]._width_of(char) @_dispatch(min=128, max=131, state=_dvistate.inpage, args=('olen1',)) def _set_char(self, char): self._put_char_real(char) - if isinstance(self.fonts[self.f], FileNotFoundError): + if isinstance(self.fonts[self.f], cbook._ExceptionInfo): return self.h += self.fonts[self.f]._width_of(char) @@ -390,7 +390,7 @@ def _put_char(self, char): def _put_char_real(self, char): font = self.fonts[self.f] - if isinstance(font, FileNotFoundError): + if isinstance(font, cbook._ExceptionInfo): self._missing_font = font elif font._vf is None: self.text.append(Text(self.h, self.v, font, char, @@ -504,7 +504,7 @@ def _fnt_def_real(self, k, c, s, d, a, l): # and throw that error in Dvi._read. For Vf, _finalize_packet # checks whether a missing glyph has been used, and in that case # skips the glyph definition. - self.fonts[k] = exc + self.fonts[k] = cbook._ExceptionInfo.from_exception(exc) return if c != 0 and tfm.checksum != 0 and c != tfm.checksum: raise ValueError(f'tfm checksum mismatch: {n}') diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 0d5a686de9d8..4271bb78e8de 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -6,9 +6,8 @@ Many methods are implemented in `FigureBase`. `SubFigure` - A logical figure inside a figure, usually added to a figure (or parent - `SubFigure`) with `Figure.add_subfigure` or `Figure.subfigures` methods - (provisional API v3.4). + A logical figure inside a figure, usually added to a figure (or parent `SubFigure`) + with `Figure.add_subfigure` or `Figure.subfigures` methods. Figures are typically created using pyplot methods `~.pyplot.figure`, `~.pyplot.subplots`, and `~.pyplot.subplot_mosaic`. @@ -614,22 +613,22 @@ def add_axes(self, *args, **kwargs): """ if not len(args) and 'rect' not in kwargs: - raise TypeError( - "add_axes() missing 1 required positional argument: 'rect'") + raise TypeError("add_axes() missing 1 required positional argument: 'rect'") elif 'rect' in kwargs: if len(args): - raise TypeError( - "add_axes() got multiple values for argument 'rect'") + raise TypeError("add_axes() got multiple values for argument 'rect'") args = (kwargs.pop('rect'), ) + if len(args) != 1: + raise _api.nargs_error("add_axes", 1, len(args)) if isinstance(args[0], Axes): - a, *extra_args = args + a, = args key = a._projection_init if a.get_figure(root=False) is not self: raise ValueError( "The Axes must have been created in the present figure") else: - rect, *extra_args = args + rect, = args if not np.isfinite(rect).all(): raise ValueError(f'all entries in rect must be finite not {rect}') projection_class, pkw = self._process_projection_requirements(**kwargs) @@ -638,11 +637,6 @@ def add_axes(self, *args, **kwargs): a = projection_class(self, rect, **pkw) key = (projection_class, pkw) - if extra_args: - _api.warn_deprecated( - "3.8", - name="Passing more than one positional argument to Figure.add_axes", - addendum="Any additional positional arguments are currently ignored.") return self._add_axes_internal(a, key) @_docstring.interpd @@ -1322,6 +1316,8 @@ def subplots_adjust(self, left=None, bottom=None, right=None, top=None, Unset parameters are left unmodified; initial values are given by :rc:`figure.subplot.[name]`. + .. plot:: _embedded_plots/figure_subplots_adjust.py + Parameters ---------- left : float, optional @@ -1608,9 +1604,6 @@ def subfigures(self, nrows=1, ncols=1, squeeze=True, the same as a figure, but cannot print itself. See :doc:`/gallery/subplots_axes_and_figures/subfigures`. - .. note:: - The *subfigure* concept is new in v3.4, and the API is still provisional. - .. versionchanged:: 3.10 subfigures are now added in row-major order. @@ -2229,9 +2222,6 @@ class SubFigure(FigureBase): axsR = sfigs[1].subplots(2, 1) See :doc:`/gallery/subplots_axes_and_figures/subfigures` - - .. note:: - The *subfigure* concept is new in v3.4, and the API is still provisional. """ def __init__(self, parent, subplotspec, *, diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index d9560ec0cc0f..98731af3463f 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -28,10 +28,10 @@ from __future__ import annotations from base64 import b64encode -from collections import namedtuple import copy import dataclasses from functools import lru_cache +import functools from io import BytesIO import json import logging @@ -132,8 +132,6 @@ 'sans', } -_ExceptionProxy = namedtuple('_ExceptionProxy', ['klass', 'message']) - # OS Font paths try: _HOME = Path.home() @@ -536,6 +534,57 @@ def afmFontProperty(fontpath, font): return FontEntry(fontpath, name, style, variant, weight, stretch, size) +def _cleanup_fontproperties_init(init_method): + """ + A decorator to limit the call signature to single a positional argument + or alternatively only keyword arguments. + + We still accept but deprecate all other call signatures. + + When the deprecation expires we can switch the signature to:: + + __init__(self, pattern=None, /, *, family=None, style=None, ...) + + plus a runtime check that pattern is not used alongside with the + keyword arguments. This results eventually in the two possible + call signatures:: + + FontProperties(pattern) + FontProperties(family=..., size=..., ...) + + """ + @functools.wraps(init_method) + def wrapper(self, *args, **kwargs): + # multiple args with at least some positional ones + if len(args) > 1 or len(args) == 1 and kwargs: + # Note: Both cases were previously handled as individual properties. + # Therefore, we do not mention the case of font properties here. + _api.warn_deprecated( + "3.10", + message="Passing individual properties to FontProperties() " + "positionally was deprecated in Matplotlib %(since)s and " + "will be removed in %(removal)s. Please pass all properties " + "via keyword arguments." + ) + # single non-string arg -> clearly a family not a pattern + if len(args) == 1 and not kwargs and not cbook.is_scalar_or_string(args[0]): + # Case font-family list passed as single argument + _api.warn_deprecated( + "3.10", + message="Passing family as positional argument to FontProperties() " + "was deprecated in Matplotlib %(since)s and will be removed " + "in %(removal)s. Please pass family names as keyword" + "argument." + ) + # Note on single string arg: + # This has been interpreted as pattern so far. We are already raising if a + # non-pattern compatible family string was given. Therefore, we do not need + # to warn for this case. + return init_method(self, *args, **kwargs) + + return wrapper + + class FontProperties: """ A class for storing and manipulating font properties. @@ -585,9 +634,14 @@ class FontProperties: approach allows all text sizes to be made larger or smaller based on the font manager's default font size. - This class will also accept a fontconfig_ pattern_, if it is the only - argument provided. This support does not depend on fontconfig; we are - merely borrowing its pattern syntax for use here. + This class accepts a single positional string as fontconfig_ pattern_, + or alternatively individual properties as keyword arguments:: + + FontProperties(pattern) + FontProperties(*, family=None, style=None, variant=None, ...) + + This support does not depend on fontconfig; we are merely borrowing its + pattern syntax for use here. .. _fontconfig: https://www.freedesktop.org/wiki/Software/fontconfig/ .. _pattern: @@ -599,6 +653,7 @@ class FontProperties: fontconfig. """ + @_cleanup_fontproperties_init def __init__(self, family=None, style=None, variant=None, weight=None, stretch=None, size=None, fname=None, # if set, it's a hardcoded filename to use @@ -1297,8 +1352,8 @@ def findfont(self, prop, fontext='ttf', directory=None, ret = self._findfont_cached( prop, fontext, directory, fallback_to_default, rebuild_if_missing, rc_params) - if isinstance(ret, _ExceptionProxy): - raise ret.klass(ret.message) + if isinstance(ret, cbook._ExceptionInfo): + raise ret.to_exception() return ret def get_font_names(self): @@ -1451,7 +1506,7 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, # This return instead of raise is intentional, as we wish to # cache that it was not found, which will not occur if it was # actually raised. - return _ExceptionProxy( + return cbook._ExceptionInfo( ValueError, f"Failed to find font {prop}, and fallback to the default font was " f"disabled" @@ -1477,7 +1532,7 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, # This return instead of raise is intentional, as we wish to # cache that it was not found, which will not occur if it was # actually raised. - return _ExceptionProxy(ValueError, "No valid font could be found") + return cbook._ExceptionInfo(ValueError, "No valid font could be found") return _cached_realpath(result) diff --git a/lib/matplotlib/hatch.py b/lib/matplotlib/hatch.py index 7a4b283c1dbe..0cbd042e1628 100644 --- a/lib/matplotlib/hatch.py +++ b/lib/matplotlib/hatch.py @@ -192,7 +192,7 @@ def _validate_hatch_pattern(hatch): message=f'hatch must consist of a string of "{valid}" or ' 'None, but found the following invalid values ' f'"{invalids}". Passing invalid values is deprecated ' - 'since %(since)s and will become an error %(removal)s.' + 'since %(since)s and will become an error in %(removal)s.' ) diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 2a7afbbe450c..03e1ed43e43a 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -919,10 +919,10 @@ def set_extent(self, extent, **kwargs): Notes ----- - This updates ``ax.dataLim``, and, if autoscaling, sets ``ax.viewLim`` - to tightly fit the image, regardless of ``dataLim``. Autoscaling - state is not changed, so following this with ``ax.autoscale_view()`` - will redo the autoscaling in accord with ``dataLim``. + This updates `.Axes.dataLim`, and, if autoscaling, sets `.Axes.viewLim` + to tightly fit the image, regardless of `~.Axes.dataLim`. Autoscaling + state is not changed, so a subsequent call to `.Axes.autoscale_view` + will redo the autoscaling in accord with `~.Axes.dataLim`. """ (xmin, xmax), (ymin, ymax) = self.axes._process_unit_info( [("x", [extent[0], extent[1]]), diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 0d487a48bde7..ace3f668e740 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -305,7 +305,7 @@ def _update_bbox_to_anchor(self, loc_in_canvas): _loc_doc_base.format(parent='axes', default=':rc:`legend.loc`', best=_loc_doc_best, outside='') + _legend_kw_doc_base) -_docstring.interpd.update(_legend_kw_axes=_legend_kw_axes_st) +_docstring.interpd.register(_legend_kw_axes=_legend_kw_axes_st) _outside_doc = """ If a figure is using the constrained layout manager, the string codes @@ -323,20 +323,20 @@ def _update_bbox_to_anchor(self, loc_in_canvas): _loc_doc_base.format(parent='figure', default="'upper right'", best='', outside=_outside_doc) + _legend_kw_doc_base) -_docstring.interpd.update(_legend_kw_figure=_legend_kw_figure_st) +_docstring.interpd.register(_legend_kw_figure=_legend_kw_figure_st) _legend_kw_both_st = ( _loc_doc_base.format(parent='axes/figure', default=":rc:`legend.loc` for Axes, 'upper right' for Figure", best=_loc_doc_best, outside=_outside_doc) + _legend_kw_doc_base) -_docstring.interpd.update(_legend_kw_doc=_legend_kw_both_st) +_docstring.interpd.register(_legend_kw_doc=_legend_kw_both_st) _legend_kw_set_loc_st = ( _loc_doc_base.format(parent='axes/figure', default=":rc:`legend.loc` for Axes, 'upper right' for Figure", best=_loc_doc_best, outside=_outside_doc)) -_docstring.interpd.update(_legend_kw_set_loc_doc=_legend_kw_set_loc_st) +_docstring.interpd.register(_legend_kw_set_loc_doc=_legend_kw_set_loc_st) class Legend(Artist): @@ -454,24 +454,10 @@ def __init__( self.borderaxespad = mpl._val_or_rc(borderaxespad, 'legend.borderaxespad') self.columnspacing = mpl._val_or_rc(columnspacing, 'legend.columnspacing') self.shadow = mpl._val_or_rc(shadow, 'legend.shadow') - # trim handles and labels if illegal label... - _lab, _hand = [], [] - for label, handle in zip(labels, handles): - if isinstance(label, str) and label.startswith('_'): - _api.warn_deprecated("3.8", message=( - "An artist whose label starts with an underscore was passed to " - "legend(); such artists will no longer be ignored in the future. " - "To suppress this warning, explicitly filter out such artists, " - "e.g. with `[art for art in artists if not " - "art.get_label().startswith('_')]`.")) - else: - _lab.append(label) - _hand.append(handle) - labels, handles = _lab, _hand if reverse: - labels.reverse() - handles.reverse() + labels = [*reversed(labels)] + handles = [*reversed(handles)] if len(handles) < 2: ncols = 1 @@ -1337,7 +1323,7 @@ def _parse_legend_args(axs, *args, handles=None, labels=None, **kwargs): _api.warn_deprecated("3.9", message=( "You have mixed positional and keyword arguments, some input may " "be discarded. This is deprecated since %(since)s and will " - "become an error %(removal)s.")) + "become an error in %(removal)s.")) if (hasattr(handles, "__len__") and hasattr(labels, "__len__") and diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index acaf6328ac49..5a7c83ccbc06 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -1521,7 +1521,7 @@ def get_transform(self): (vxlo, vylo), (vxhi, vyhi) = ax.transScale.transform(ax.viewLim) # General case: find intersections with view limits in either # direction, and draw between the middle two points. - if np.isclose(slope, 0): + if slope == 0: start = vxlo, y1 stop = vxhi, y1 elif np.isinf(slope): @@ -1553,18 +1553,28 @@ def get_slope(self): """Return the *slope* value of the line.""" return self._slope - def set_xy1(self, x, y): + def set_xy1(self, *args, **kwargs): """ Set the *xy1* value of the line. Parameters ---------- - x, y : float + xy1 : tuple[float, float] Points for the line to pass through. """ - self._xy1 = x, y + params = _api.select_matching_signature([ + lambda self, x, y: locals(), lambda self, xy1: locals(), + ], self, *args, **kwargs) + if "x" in params: + _api.warn_deprecated("3.10", message=( + "Passing x and y separately to AxLine.set_xy1 is deprecated since " + "%(since)s; pass them as a single tuple instead.")) + xy1 = params["x"], params["y"] + else: + xy1 = params["xy1"] + self._xy1 = xy1 - def set_xy2(self, x, y): + def set_xy2(self, *args, **kwargs): """ Set the *xy2* value of the line. @@ -1576,11 +1586,21 @@ def set_xy2(self, x, y): Parameters ---------- - x, y : float + xy2 : tuple[float, float] Points for the line to pass through. """ if self._slope is None: - self._xy2 = x, y + params = _api.select_matching_signature([ + lambda self, x, y: locals(), lambda self, xy2: locals(), + ], self, *args, **kwargs) + if "x" in params: + _api.warn_deprecated("3.10", message=( + "Passing x and y separately to AxLine.set_xy2 is deprecated since " + "%(since)s; pass them as a single tuple instead.")) + xy2 = params["x"], params["y"] + else: + xy2 = params["xy2"] + self._xy2 = xy2 else: raise ValueError("Cannot set an 'xy2' value while 'slope' is set;" " they differ but their functionalities overlap") diff --git a/lib/matplotlib/lines.pyi b/lib/matplotlib/lines.pyi index 161f99100bf5..7989a03dae3a 100644 --- a/lib/matplotlib/lines.pyi +++ b/lib/matplotlib/lines.pyi @@ -130,8 +130,8 @@ class AxLine(Line2D): def get_xy1(self) -> tuple[float, float] | None: ... def get_xy2(self) -> tuple[float, float] | None: ... def get_slope(self) -> float: ... - def set_xy1(self, x: float, y: float) -> None: ... - def set_xy2(self, x: float, y: float) -> None: ... + def set_xy1(self, xy1: tuple[float, float]) -> None: ... + def set_xy2(self, xy2: tuple[float, float]) -> None: ... def set_slope(self, slope: float) -> None: ... class VertexSelector: diff --git a/lib/matplotlib/mlab.py b/lib/matplotlib/mlab.py index fad8d648f6db..8326ac186e31 100644 --- a/lib/matplotlib/mlab.py +++ b/lib/matplotlib/mlab.py @@ -400,7 +400,7 @@ def _single_spectrum_helper( # Split out these keyword docs so that they can be used elsewhere -_docstring.interpd.update( +_docstring.interpd.register( Spectral="""\ Fs : float, default: 2 The sampling frequency (samples per time unit). It is used to calculate diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 1c19d8424db0..2db678587ec7 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -1552,7 +1552,7 @@ def _make_verts(self): ] -_docstring.interpd.update( +_docstring.interpd.register( FancyArrow="\n".join( (inspect.getdoc(FancyArrow.__init__) or "").splitlines()[2:])) @@ -2290,7 +2290,7 @@ def __init_subclass__(cls): # - %(BoxStyle:table_and_accepts)s # - %(ConnectionStyle:table_and_accepts)s # - %(ArrowStyle:table_and_accepts)s - _docstring.interpd.update({ + _docstring.interpd.register(**{ f"{cls.__name__}:table": cls.pprint_styles(), f"{cls.__name__}:table_and_accepts": ( cls.pprint_styles() diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 0e870161a48d..5f5a0f3de423 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -1086,10 +1086,7 @@ def get_path_collection_extents( if len(paths) == 0: raise ValueError("No paths provided") if len(offsets) == 0: - _api.warn_deprecated( - "3.8", message="Calling get_path_collection_extents() with an" - " empty offsets list is deprecated since %(since)s. Support will" - " be removed %(removal)s.") + raise ValueError("No offsets provided") extents, minpos = _path.get_path_collection_extents( master_transform, paths, np.atleast_3d(transforms), offsets, offset_transform) diff --git a/lib/matplotlib/projections/__init__.py b/lib/matplotlib/projections/__init__.py index b58d1ceb754d..f7b46192a84e 100644 --- a/lib/matplotlib/projections/__init__.py +++ b/lib/matplotlib/projections/__init__.py @@ -123,4 +123,4 @@ def get_projection_class(projection=None): get_projection_names = projection_registry.get_projection_names -_docstring.interpd.update(projection_names=get_projection_names()) +_docstring.interpd.register(projection_names=get_projection_names()) diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 325da95105ab..7fe6045039b1 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -21,7 +21,7 @@ def _apply_theta_transforms_warn(): message=( "Passing `apply_theta_transforms=True` (the default) " "is deprecated since Matplotlib %(since)s. " - "Support for this will be removed in Matplotlib %(removal)s. " + "Support for this will be removed in Matplotlib in %(removal)s. " "To prevent this warning, set `apply_theta_transforms=False`, " "and make sure to shift theta values before being passed to " "this transform." diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 744eee0e4b9f..69c80e6d3579 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -514,14 +514,6 @@ def draw_if_interactive() -> None: # See https://github.com/matplotlib/matplotlib/issues/6092 matplotlib.backends.backend = newbackend # type: ignore[attr-defined] - if not cbook._str_equal(old_backend, newbackend): - if get_fignums(): - _api.warn_deprecated("3.8", message=( - "Auto-close()ing of figures upon backend switching is deprecated since " - "%(since)s and will be removed %(removal)s. To suppress this warning, " - "explicitly call plt.close('all') first.")) - close("all") - # Make sure the repl display hook is installed in case we become interactive. install_repl_displayhook() diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index 16c8a2195f67..c7408476c784 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -230,7 +230,7 @@ of the head in forward direction so that the arrow head looks broken. """ % _docstring.interpd.params -_docstring.interpd.update(quiver_doc=_quiver_doc) +_docstring.interpd.register(quiver_doc=_quiver_doc) class QuiverKey(martist.Artist): @@ -865,7 +865,7 @@ def _h_arrows(self, length): %(PolyCollection:kwdoc)s """ % _docstring.interpd.params -_docstring.interpd.update(barbs_doc=_barbs_doc) +_docstring.interpd.register(barbs_doc=_barbs_doc) class Barbs(mcollections.PolyCollection): diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index e84b0539385b..308e02fca72b 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -463,19 +463,6 @@ def validate_ps_distiller(s): return ValidateInStrings('ps.usedistiller', ['ghostscript', 'xpdf'])(s) -def _validate_papersize(s): - # Re-inline this validator when the 'auto' deprecation expires. - s = ValidateInStrings("ps.papersize", - ["figure", "auto", "letter", "legal", "ledger", - *[f"{ab}{i}" for ab in "ab" for i in range(11)]], - ignorecase=True)(s) - if s == "auto": - _api.warn_deprecated("3.8", name="ps.papersize='auto'", - addendum="Pass an explicit paper type, figure, or omit " - "the *ps.papersize* rcParam entirely.") - return s - - # A validator dedicated to the named line styles, based on the items in # ls_mapper, and a list of possible strings read from Line2D.set_linestyle _validate_named_linestyle = ValidateInStrings( @@ -1291,7 +1278,9 @@ def _convert_validator_spec(key, conv): "tk.window_focus": validate_bool, # Maintain shell focus for TkAgg # Set the papersize/type - "ps.papersize": _validate_papersize, + "ps.papersize": _ignorecase( + ["figure", "letter", "legal", "ledger", + *[f"{ab}{i}" for ab in "ab" for i in range(11)]]), "ps.useafm": validate_bool, # use ghostscript or xpdf to distill ps output "ps.usedistiller": validate_ps_distiller, diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index f81137c75082..ccaaae6caf5d 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -742,7 +742,7 @@ def _get_scale_docs(): return "\n".join(docs) -_docstring.interpd.update( +_docstring.interpd.register( scale_type='{%s}' % ', '.join([repr(x) for x in get_scale_names()]), scale_docs=_get_scale_docs().rstrip(), ) diff --git a/lib/matplotlib/table.py b/lib/matplotlib/table.py index 0f75021926fd..212cd9f45187 100644 --- a/lib/matplotlib/table.py +++ b/lib/matplotlib/table.py @@ -496,11 +496,7 @@ def auto_set_column_width(self, col): """ col1d = np.atleast_1d(col) if not np.issubdtype(col1d.dtype, np.integer): - _api.warn_deprecated("3.8", name="col", - message="%(name)r must be an int or sequence of ints. " - "Passing other types is deprecated since %(since)s " - "and will be removed %(removal)s.") - return + raise TypeError("col must be an int or sequence of ints.") for cell in col1d: self._autoColumns.append(cell) diff --git a/lib/matplotlib/testing/conftest.py b/lib/matplotlib/testing/conftest.py index c285c247e7b4..3f96de611195 100644 --- a/lib/matplotlib/testing/conftest.py +++ b/lib/matplotlib/testing/conftest.py @@ -82,7 +82,20 @@ def mpl_test_settings(request): @pytest.fixture def pd(): - """Fixture to import and configure pandas.""" + """ + Fixture to import and configure pandas. Using this fixture, the test is skipped when + pandas is not installed. Use this fixture instead of importing pandas in test files. + + Examples + -------- + Request the pandas fixture by passing in ``pd`` as an argument to the test :: + + def test_matshow_pandas(pd): + + df = pd.DataFrame({'x':[1,2,3], 'y':[4,5,6]}) + im = plt.figure().subplots().matshow(df) + np.testing.assert_array_equal(im.get_array(), df) + """ pd = pytest.importorskip('pandas') try: from pandas.plotting import ( @@ -95,6 +108,20 @@ def pd(): @pytest.fixture def xr(): - """Fixture to import xarray.""" + """ + Fixture to import xarray so that the test is skipped when xarray is not installed. + Use this fixture instead of importing xrray in test files. + + Examples + -------- + Request the xarray fixture by passing in ``xr`` as an argument to the test :: + + def test_imshow_xarray(xr): + + ds = xr.DataArray(np.random.randn(2, 3)) + im = plt.figure().subplots().imshow(ds) + np.testing.assert_array_equal(im.get_array(), ds) + """ + xr = pytest.importorskip('xarray') return xr diff --git a/lib/matplotlib/tests/baseline_images/test_polar/polar_errorbar.png b/lib/matplotlib/tests/baseline_images/test_polar/polar_errorbar.png new file mode 100644 index 000000000000..6c75ee24aee2 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_polar/polar_errorbar.png differ diff --git a/lib/matplotlib/tests/test_agg.py b/lib/matplotlib/tests/test_agg.py index 80c1f165382c..59387793605a 100644 --- a/lib/matplotlib/tests/test_agg.py +++ b/lib/matplotlib/tests/test_agg.py @@ -199,7 +199,7 @@ def process_image(self, padded_src, dpi): def test_too_large_image(): - fig = plt.figure(figsize=(300, 1000)) + fig = plt.figure(figsize=(300, 2**25)) buff = io.BytesIO() with pytest.raises(ValueError): fig.savefig(buff) diff --git a/lib/matplotlib/tests/test_api.py b/lib/matplotlib/tests/test_api.py index 23d3ec48f31f..f04604c14cce 100644 --- a/lib/matplotlib/tests/test_api.py +++ b/lib/matplotlib/tests/test_api.py @@ -49,6 +49,41 @@ def f(cls: Self) -> None: a.f +def test_warn_deprecated(): + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'foo was deprecated in Matplotlib 3\.10 and will be ' + r'removed in 3\.12\.'): + _api.warn_deprecated('3.10', name='foo') + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'The foo class was deprecated in Matplotlib 3\.10 and ' + r'will be removed in 3\.12\.'): + _api.warn_deprecated('3.10', name='foo', obj_type='class') + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'foo was deprecated in Matplotlib 3\.10 and will be ' + r'removed in 3\.12\. Use bar instead\.'): + _api.warn_deprecated('3.10', name='foo', alternative='bar') + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'foo was deprecated in Matplotlib 3\.10 and will be ' + r'removed in 3\.12\. More information\.'): + _api.warn_deprecated('3.10', name='foo', addendum='More information.') + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'foo was deprecated in Matplotlib 3\.10 and will be ' + r'removed in 4\.0\.'): + _api.warn_deprecated('3.10', name='foo', removal='4.0') + with pytest.warns(mpl.MatplotlibDeprecationWarning, + match=r'foo was deprecated in Matplotlib 3\.10\.'): + _api.warn_deprecated('3.10', name='foo', removal=False) + with pytest.warns(PendingDeprecationWarning, + match=r'foo will be deprecated in a future version'): + _api.warn_deprecated('3.10', name='foo', pending=True) + with pytest.raises(ValueError, match=r'cannot have a scheduled removal'): + _api.warn_deprecated('3.10', name='foo', pending=True, removal='3.12') + with pytest.warns(mpl.MatplotlibDeprecationWarning, match=r'Complete replacement'): + _api.warn_deprecated('3.10', message='Complete replacement', name='foo', + alternative='bar', addendum='More information.', + obj_type='class', removal='4.0') + + def test_deprecate_privatize_attribute() -> None: class C: def __init__(self) -> None: self._attr = 1 diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 33c81c44abaf..5d50df4ffd20 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -136,20 +136,20 @@ def test_label_shift(): # Test label re-centering on x-axis ax.set_xlabel("Test label", loc="left") ax.set_xlabel("Test label", loc="center") - assert ax.xaxis.get_label().get_horizontalalignment() == "center" + assert ax.xaxis.label.get_horizontalalignment() == "center" ax.set_xlabel("Test label", loc="right") - assert ax.xaxis.get_label().get_horizontalalignment() == "right" + assert ax.xaxis.label.get_horizontalalignment() == "right" ax.set_xlabel("Test label", loc="center") - assert ax.xaxis.get_label().get_horizontalalignment() == "center" + assert ax.xaxis.label.get_horizontalalignment() == "center" # Test label re-centering on y-axis ax.set_ylabel("Test label", loc="top") ax.set_ylabel("Test label", loc="center") - assert ax.yaxis.get_label().get_horizontalalignment() == "center" + assert ax.yaxis.label.get_horizontalalignment() == "center" ax.set_ylabel("Test label", loc="bottom") - assert ax.yaxis.get_label().get_horizontalalignment() == "left" + assert ax.yaxis.label.get_horizontalalignment() == "left" ax.set_ylabel("Test label", loc="center") - assert ax.yaxis.get_label().get_horizontalalignment() == "center" + assert ax.yaxis.label.get_horizontalalignment() == "center" @check_figures_equal(extensions=["png"]) @@ -6149,6 +6149,27 @@ def test_pie_get_negative_values(): ax.pie([5, 5, -3], explode=[0, .1, .2]) +def test_pie_invalid_explode(): + # Test ValueError raised when feeding short explode list to axes.pie + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.pie([1, 2, 3], explode=[0.1, 0.1]) + + +def test_pie_invalid_labels(): + # Test ValueError raised when feeding short labels list to axes.pie + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.pie([1, 2, 3], labels=["One", "Two"]) + + +def test_pie_invalid_radius(): + # Test ValueError raised when feeding negative radius to axes.pie + fig, ax = plt.subplots() + with pytest.raises(ValueError): + ax.pie([1, 2, 3], radius=-5) + + def test_normalize_kwarg_pie(): fig, ax = plt.subplots() x = [0.3, 0.3, 0.1] @@ -6626,6 +6647,27 @@ def test_pcolorfast_bad_dims(): ax.pcolorfast(np.empty(6), np.empty((4, 7)), np.empty((8, 8))) +def test_pcolorfast_regular_xy_incompatible_size(): + """ + Test that the sizes of X, Y, C are compatible for regularly spaced X, Y. + + Note that after the regualar-spacing check, pcolorfast may go into the + fast "image" mode, where the individual X, Y positions are not used anymore. + Therefore, the algorithm had worked with any regularly number of regularly + spaced values, but discarded their values. + """ + fig, ax = plt.subplots() + with pytest.raises( + ValueError, match=r"Length of X \(5\) must be one larger than the " + r"number of columns in C \(20\)"): + ax.pcolorfast(np.arange(5), np.arange(11), np.random.rand(10, 20)) + + with pytest.raises( + ValueError, match=r"Length of Y \(5\) must be one larger than the " + r"number of rows in C \(10\)"): + ax.pcolorfast(np.arange(21), np.arange(5), np.random.rand(10, 20)) + + def test_shared_scale(): fig, axs = plt.subplots(2, 2, sharex=True, sharey=True) @@ -8421,7 +8463,7 @@ def test_ylabel_ha_with_position(ha): ax = fig.subplots() ax.set_ylabel("test", y=1, ha=ha) ax.yaxis.set_label_position("right") - assert ax.yaxis.get_label().get_ha() == ha + assert ax.yaxis.label.get_ha() == ha def test_bar_label_location_vertical(): diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 6a6dc1a6bac1..3fcf124e364d 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -81,48 +81,18 @@ def test_multipage_properfinalize(): def test_multipage_keep_empty(tmp_path): - # test empty pdf files - - # an empty pdf is left behind with keep_empty unset + # An empty pdf deletes itself afterwards. fn = tmp_path / "a.pdf" - with pytest.warns(mpl.MatplotlibDeprecationWarning), PdfPages(fn) as pdf: - pass - assert fn.exists() - - # an empty pdf is left behind with keep_empty=True - fn = tmp_path / "b.pdf" - with (pytest.warns(mpl.MatplotlibDeprecationWarning), - PdfPages(fn, keep_empty=True) as pdf): - pass - assert fn.exists() - - # an empty pdf deletes itself afterwards with keep_empty=False - fn = tmp_path / "c.pdf" - with PdfPages(fn, keep_empty=False) as pdf: + with PdfPages(fn) as pdf: pass assert not fn.exists() - # test pdf files with content, they should never be deleted - - # a non-empty pdf is left behind with keep_empty unset - fn = tmp_path / "d.pdf" + # Test pdf files with content, they should never be deleted. + fn = tmp_path / "b.pdf" with PdfPages(fn) as pdf: pdf.savefig(plt.figure()) assert fn.exists() - # a non-empty pdf is left behind with keep_empty=True - fn = tmp_path / "e.pdf" - with (pytest.warns(mpl.MatplotlibDeprecationWarning), - PdfPages(fn, keep_empty=True) as pdf): - pdf.savefig(plt.figure()) - assert fn.exists() - - # a non-empty pdf is left behind with keep_empty=False - fn = tmp_path / "f.pdf" - with PdfPages(fn, keep_empty=False) as pdf: - pdf.savefig(plt.figure()) - assert fn.exists() - def test_composite_image(): # Test that figures can be saved with and without combining multiple images diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index 54b1c3b5896e..e218a81cdceb 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -288,48 +288,18 @@ def test_pdf_pages_metadata_check(monkeypatch, system): @needs_pgf_xelatex def test_multipage_keep_empty(tmp_path): - # test empty pdf files - - # an empty pdf is left behind with keep_empty unset + # An empty pdf deletes itself afterwards. fn = tmp_path / "a.pdf" - with pytest.warns(mpl.MatplotlibDeprecationWarning), PdfPages(fn) as pdf: - pass - assert fn.exists() - - # an empty pdf is left behind with keep_empty=True - fn = tmp_path / "b.pdf" - with (pytest.warns(mpl.MatplotlibDeprecationWarning), - PdfPages(fn, keep_empty=True) as pdf): - pass - assert fn.exists() - - # an empty pdf deletes itself afterwards with keep_empty=False - fn = tmp_path / "c.pdf" - with PdfPages(fn, keep_empty=False) as pdf: + with PdfPages(fn) as pdf: pass assert not fn.exists() - # test pdf files with content, they should never be deleted - - # a non-empty pdf is left behind with keep_empty unset - fn = tmp_path / "d.pdf" + # Test pdf files with content, they should never be deleted. + fn = tmp_path / "b.pdf" with PdfPages(fn) as pdf: pdf.savefig(plt.figure()) assert fn.exists() - # a non-empty pdf is left behind with keep_empty=True - fn = tmp_path / "e.pdf" - with (pytest.warns(mpl.MatplotlibDeprecationWarning), - PdfPages(fn, keep_empty=True) as pdf): - pdf.savefig(plt.figure()) - assert fn.exists() - - # a non-empty pdf is left behind with keep_empty=False - fn = tmp_path / "f.pdf" - with PdfPages(fn, keep_empty=False) as pdf: - pdf.savefig(plt.figure()) - assert fn.exists() - @needs_pgf_xelatex def test_tex_restart_after_error(): diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index c587a00c0af9..cc968795802e 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -371,10 +371,10 @@ def test_colorbar_shift(tmp_path): plt.colorbar() -def test_auto_papersize_deprecation(): +def test_auto_papersize_removal(): fig = plt.figure() - with pytest.warns(mpl.MatplotlibDeprecationWarning): + with pytest.raises(ValueError, match="'auto' is not a valid value"): fig.savefig(io.BytesIO(), format='eps', papertype='auto') - with pytest.warns(mpl.MatplotlibDeprecationWarning): + with pytest.raises(ValueError, match="'auto' is not a valid value"): mpl.rcParams['ps.papersize'] = 'auto' diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index ee20a94042f7..89782e8a66f3 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -35,8 +35,8 @@ def _isolated_tk_test(success_count, func=None): reason="missing tkinter" ) @pytest.mark.skipif( - sys.platform == "linux" and not _c_internal_utils.display_is_valid(), - reason="$DISPLAY and $WAYLAND_DISPLAY are unset" + sys.platform == "linux" and not _c_internal_utils.xdisplay_is_valid(), + reason="$DISPLAY is unset" ) @pytest.mark.xfail( # https://github.com/actions/setup-python/issues/649 ('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 2c6b61a48438..7621ac5b5689 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -57,6 +57,8 @@ def wait_for(self, terminator): def _get_available_interactive_backends(): _is_linux_and_display_invalid = (sys.platform == "linux" and not _c_internal_utils.display_is_valid()) + _is_linux_and_xdisplay_invalid = (sys.platform == "linux" and + not _c_internal_utils.xdisplay_is_valid()) envs = [] for deps, env in [ *[([qt_api], @@ -74,10 +76,12 @@ def _get_available_interactive_backends(): ]: reason = None missing = [dep for dep in deps if not importlib.util.find_spec(dep)] - if _is_linux_and_display_invalid: - reason = "$DISPLAY and $WAYLAND_DISPLAY are unset" - elif missing: + if missing: reason = "{} cannot be imported".format(", ".join(missing)) + elif env["MPLBACKEND"] == "tkagg" and _is_linux_and_xdisplay_invalid: + reason = "$DISPLAY is unset" + elif _is_linux_and_display_invalid: + reason = "$DISPLAY and $WAYLAND_DISPLAY are unset" elif env["MPLBACKEND"] == 'macosx' and os.environ.get('TF_BUILD'): reason = "macosx backend fails on Azure" elif env["MPLBACKEND"].startswith('gtk'): @@ -166,7 +170,8 @@ def _test_interactive_impl(): if backend.endswith("agg") and not backend.startswith(("gtk", "web")): # Force interactive framework setup. - plt.figure() + fig = plt.figure() + plt.close(fig) # Check that we cannot switch to a backend using another interactive # framework, but can switch to a backend using cairo instead of agg, @@ -224,10 +229,7 @@ def check_alt_backend(alt_backend): result_after = io.BytesIO() fig.savefig(result_after, format='png') - if not backend.startswith('qt5') and sys.platform == 'darwin': - # FIXME: This should be enabled everywhere once Qt5 is fixed on macOS - # to not resize incorrectly. - assert result.getvalue() == result_after.getvalue() + assert result.getvalue() == result_after.getvalue() @pytest.mark.parametrize("env", _get_testable_interactive_backends()) @@ -444,8 +446,7 @@ def qt5_and_qt6_pairs(): for qt5 in qt5_bindings: for qt6 in qt6_bindings: - for pair in ([qt5, qt6], [qt6, qt5]): - yield pair + yield from ([qt5, qt6], [qt6, qt5]) @pytest.mark.parametrize('host, mpl', [*qt5_and_qt6_pairs()]) diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index 222cc23b7e4d..435745e03e16 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -181,6 +181,15 @@ def test_boxplot_stats_autorange_false(self): assert_array_almost_equal(bstats_true[0]['fliers'], []) +class Hashable: + def dummy(self): pass + + +class Unhashable: + __hash__ = None # type: ignore + def dummy(self): pass + + class Test_callback_registry: def setup_method(self): self.signal = 'test' @@ -196,20 +205,20 @@ def disconnect(self, cid): return self.callbacks.disconnect(cid) def count(self): - count1 = len(self.callbacks._func_cid_map.get(self.signal, [])) + count1 = sum(s == self.signal for s, p in self.callbacks._func_cid_map) count2 = len(self.callbacks.callbacks.get(self.signal)) assert count1 == count2 return count1 def is_empty(self): np.testing.break_cycles() - assert self.callbacks._func_cid_map == {} + assert [*self.callbacks._func_cid_map] == [] assert self.callbacks.callbacks == {} assert self.callbacks._pickled_cids == set() def is_not_empty(self): np.testing.break_cycles() - assert self.callbacks._func_cid_map != {} + assert [*self.callbacks._func_cid_map] != [] assert self.callbacks.callbacks != {} def test_cid_restore(self): @@ -220,12 +229,13 @@ def test_cid_restore(self): assert cid == 1 @pytest.mark.parametrize('pickle', [True, False]) - def test_callback_complete(self, pickle): + @pytest.mark.parametrize('cls', [Hashable, Unhashable]) + def test_callback_complete(self, pickle, cls): # ensure we start with an empty registry self.is_empty() # create a class for testing - mini_me = Test_callback_registry() + mini_me = cls() # test that we can add a callback cid1 = self.connect(self.signal, mini_me.dummy, pickle) @@ -236,7 +246,7 @@ def test_callback_complete(self, pickle): cid2 = self.connect(self.signal, mini_me.dummy, pickle) assert cid1 == cid2 self.is_not_empty() - assert len(self.callbacks._func_cid_map) == 1 + assert len([*self.callbacks._func_cid_map]) == 1 assert len(self.callbacks.callbacks) == 1 del mini_me @@ -245,12 +255,13 @@ def test_callback_complete(self, pickle): self.is_empty() @pytest.mark.parametrize('pickle', [True, False]) - def test_callback_disconnect(self, pickle): + @pytest.mark.parametrize('cls', [Hashable, Unhashable]) + def test_callback_disconnect(self, pickle, cls): # ensure we start with an empty registry self.is_empty() # create a class for testing - mini_me = Test_callback_registry() + mini_me = cls() # test that we can add a callback cid1 = self.connect(self.signal, mini_me.dummy, pickle) @@ -263,12 +274,13 @@ def test_callback_disconnect(self, pickle): self.is_empty() @pytest.mark.parametrize('pickle', [True, False]) - def test_callback_wrong_disconnect(self, pickle): + @pytest.mark.parametrize('cls', [Hashable, Unhashable]) + def test_callback_wrong_disconnect(self, pickle, cls): # ensure we start with an empty registry self.is_empty() # create a class for testing - mini_me = Test_callback_registry() + mini_me = cls() # test that we can add a callback cid1 = self.connect(self.signal, mini_me.dummy, pickle) @@ -281,20 +293,21 @@ def test_callback_wrong_disconnect(self, pickle): self.is_not_empty() @pytest.mark.parametrize('pickle', [True, False]) - def test_registration_on_non_empty_registry(self, pickle): + @pytest.mark.parametrize('cls', [Hashable, Unhashable]) + def test_registration_on_non_empty_registry(self, pickle, cls): # ensure we start with an empty registry self.is_empty() # setup the registry with a callback - mini_me = Test_callback_registry() + mini_me = cls() self.connect(self.signal, mini_me.dummy, pickle) # Add another callback - mini_me2 = Test_callback_registry() + mini_me2 = cls() self.connect(self.signal, mini_me2.dummy, pickle) # Remove and add the second callback - mini_me2 = Test_callback_registry() + mini_me2 = cls() self.connect(self.signal, mini_me2.dummy, pickle) # We still have 2 references @@ -306,9 +319,6 @@ def test_registration_on_non_empty_registry(self, pickle): mini_me2 = None self.is_empty() - def dummy(self): - pass - def test_pickling(self): assert hasattr(pickle.loads(pickle.dumps(cbook.CallbackRegistry())), "callbacks") diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index 99045e773d02..528df182a2d0 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -518,12 +518,10 @@ def test_invalid_figure_add_axes(): fig.add_axes(ax) fig2.delaxes(ax) - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="Passing more than one positional argument"): + with pytest.raises(TypeError, match=r"add_axes\(\) takes 1 positional arguments"): fig2.add_axes(ax, "extra positional argument") - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="Passing more than one positional argument"): + with pytest.raises(TypeError, match=r"add_axes\(\) takes 1 positional arguments"): fig.add_axes([0, 0, 1, 1], "extra positional argument") diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index ab8c6c70d1bf..25b6a122c7ce 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -11,6 +11,7 @@ import numpy as np import pytest +import matplotlib as mpl from matplotlib.font_manager import ( findfont, findSystemFonts, FontEntry, FontProperties, fontManager, json_dump, json_load, get_font, is_opentype_cff_font, @@ -367,3 +368,42 @@ def inner(): for obj in gc.get_objects(): if isinstance(obj, SomeObject): pytest.fail("object from inner stack still alive") + + +def test_fontproperties_init_deprecation(): + """ + Test the deprecated API of FontProperties.__init__. + + The deprecation does not change behavior, it only adds a deprecation warning + via a decorator. Therefore, the purpose of this test is limited to check + which calls do and do not issue deprecation warnings. Behavior is still + tested via the existing regular tests. + """ + with pytest.warns(mpl.MatplotlibDeprecationWarning): + # multiple positional arguments + FontProperties("Times", "italic") + + with pytest.warns(mpl.MatplotlibDeprecationWarning): + # Mixed positional and keyword arguments + FontProperties("Times", size=10) + + with pytest.warns(mpl.MatplotlibDeprecationWarning): + # passing a family list positionally + FontProperties(["Times"]) + + # still accepted: + FontProperties(family="Times", style="italic") + FontProperties(family="Times") + FontProperties("Times") # works as pattern and family + FontProperties("serif-24:style=oblique:weight=bold") # pattern + + # also still accepted: + # passing as pattern via family kwarg was not covered by the docs but + # historically worked. This is left unchanged for now. + # AFAICT, we cannot detect this: We can determine whether a string + # works as pattern, but that doesn't help, because there are strings + # that are both pattern and family. We would need to identify, whether + # a string is *not* a valid family. + # Since this case is not covered by docs, I've refrained from jumping + # extra hoops to detect this possible API misuse. + FontProperties(family="serif-24:style=oblique:weight=bold") diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index f083c8374619..cf0a950de1ca 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -19,7 +19,7 @@ import matplotlib.lines as mlines from matplotlib.legend_handler import HandlerTuple import matplotlib.legend as mlegend -from matplotlib import _api, rc_context +from matplotlib import rc_context from matplotlib.font_manager import FontProperties @@ -138,19 +138,6 @@ def test_various_labels(): ax.legend(numpoints=1, loc='best') -def test_legend_label_with_leading_underscore(): - """ - Test that artists with labels starting with an underscore are not added to - the legend, and that a warning is issued if one tries to add them - explicitly. - """ - fig, ax = plt.subplots() - line, = ax.plot([0, 1], label='_foo') - with pytest.warns(_api.MatplotlibDeprecationWarning, match="with an underscore"): - legend = ax.legend(handles=[line]) - assert len(legend.legend_handles) == 0 - - @image_comparison(['legend_labels_first.png'], remove_text=True, tol=0.013 if platform.machine() == 'arm64' else 0) def test_labels_first(): @@ -868,8 +855,8 @@ def test_legend_pathcollection_labelcolor_linecolor_iterable(): # test the labelcolor for labelcolor='linecolor' on PathCollection # with iterable colors fig, ax = plt.subplots() - colors = np.random.default_rng().choice(['r', 'g', 'b'], 10) - ax.scatter(np.arange(10), np.arange(10)*1, label='#1', c=colors) + colors = np.array(['r', 'g', 'b', 'c', 'm'] * 2) + ax.scatter(np.arange(10), np.arange(10), label='#1', c=colors) leg = ax.legend(labelcolor='linecolor') text, = leg.get_texts() @@ -915,8 +902,8 @@ def test_legend_pathcollection_labelcolor_markeredgecolor_iterable(): # test the labelcolor for labelcolor='markeredgecolor' on PathCollection # with iterable colors fig, ax = plt.subplots() - colors = np.random.default_rng().choice(['r', 'g', 'b'], 10) - ax.scatter(np.arange(10), np.arange(10)*1, label='#1', edgecolor=colors) + colors = np.array(['r', 'g', 'b', 'c', 'm'] * 2) + ax.scatter(np.arange(10), np.arange(10), label='#1', edgecolor=colors) leg = ax.legend(labelcolor='markeredgecolor') for text, color in zip(leg.get_texts(), ['k']): @@ -970,8 +957,8 @@ def test_legend_pathcollection_labelcolor_markerfacecolor_iterable(): # test the labelcolor for labelcolor='markerfacecolor' on PathCollection # with iterable colors fig, ax = plt.subplots() - colors = np.random.default_rng().choice(['r', 'g', 'b'], 10) - ax.scatter(np.arange(10), np.arange(10)*1, label='#1', facecolor=colors) + colors = np.array(['r', 'g', 'b', 'c', 'm'] * 2) + ax.scatter(np.arange(10), np.arange(10), label='#1', facecolor=colors) leg = ax.legend(labelcolor='markerfacecolor') for text, color in zip(leg.get_texts(), ['k']): diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index 531237b2ba28..ee8b5b4aaa9e 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -417,16 +417,20 @@ def test_axline_setters(): line2 = ax.axline((.1, .1), (.8, .4)) # Testing xy1, xy2 and slope setters. # This should not produce an error. - line1.set_xy1(.2, .3) + line1.set_xy1((.2, .3)) line1.set_slope(2.4) - line2.set_xy1(.3, .2) - line2.set_xy2(.6, .8) + line2.set_xy1((.3, .2)) + line2.set_xy2((.6, .8)) # Testing xy1, xy2 and slope getters. # Should return the modified values. assert line1.get_xy1() == (.2, .3) assert line1.get_slope() == 2.4 assert line2.get_xy1() == (.3, .2) assert line2.get_xy2() == (.6, .8) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + line1.set_xy1(.2, .3) + with pytest.warns(mpl.MatplotlibDeprecationWarning): + line2.set_xy2(.6, .8) # Testing setting xy2 and slope together. # These test should raise a ValueError with pytest.raises(ValueError, @@ -436,3 +440,14 @@ def test_axline_setters(): with pytest.raises(ValueError, match="Cannot set a 'slope' value while 'xy2' is set"): line2.set_slope(3) + + +def test_axline_small_slope(): + """Test that small slopes are not coerced to zero in the transform.""" + line = plt.axline((0, 0), slope=1e-14) + p1 = line.get_transform().transform_point((0, 0)) + p2 = line.get_transform().transform_point((1, 1)) + # y-values must be slightly different + dy = p2[1] - p1[1] + assert dy > 0 + assert dy < 4e-12 diff --git a/lib/matplotlib/tests/test_polar.py b/lib/matplotlib/tests/test_polar.py index 0bb41a50b2d4..ee38c88a123f 100644 --- a/lib/matplotlib/tests/test_polar.py +++ b/lib/matplotlib/tests/test_polar.py @@ -481,3 +481,21 @@ def test_polar_neg_theta_lims(): ax.set_thetalim(-np.pi, np.pi) labels = [l.get_text() for l in ax.xaxis.get_ticklabels()] assert labels == ['-180°', '-135°', '-90°', '-45°', '0°', '45°', '90°', '135°'] + + +@pytest.mark.parametrize("order", ["before", "after"]) +@image_comparison(baseline_images=['polar_errorbar'], remove_text=True, + extensions=['png'], style='mpl20') +def test_polar_errorbar(order): + theta = np.arange(0, 2 * np.pi, np.pi / 8) + r = theta / np.pi / 2 + 0.5 + fig = plt.figure(figsize=(5, 5)) + ax = fig.add_subplot(projection='polar') + if order == "before": + ax.set_theta_zero_location("N") + ax.set_theta_direction(-1) + ax.errorbar(theta, r, xerr=0.1, yerr=0.1, capsize=7, fmt="o", c="seagreen") + else: + ax.errorbar(theta, r, xerr=0.1, yerr=0.1, capsize=7, fmt="o", c="seagreen") + ax.set_theta_zero_location("N") + ax.set_theta_direction(-1) diff --git a/lib/matplotlib/tests/test_pyplot.py b/lib/matplotlib/tests/test_pyplot.py index 63dc239df2e8..21036e177045 100644 --- a/lib/matplotlib/tests/test_pyplot.py +++ b/lib/matplotlib/tests/test_pyplot.py @@ -439,9 +439,8 @@ def test_switch_backend_no_close(): assert len(plt.get_fignums()) == 2 plt.switch_backend('agg') assert len(plt.get_fignums()) == 2 - with pytest.warns(mpl.MatplotlibDeprecationWarning): - plt.switch_backend('svg') - assert len(plt.get_fignums()) == 0 + plt.switch_backend('svg') + assert len(plt.get_fignums()) == 2 def figure_hook_example(figure): diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 4823df0ce250..0aa3ec0ba603 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -5,6 +5,7 @@ from unittest import mock from cycler import cycler, Cycler +from packaging.version import parse as parse_version import pytest import matplotlib as mpl @@ -536,10 +537,15 @@ def test_backend_fallback_headless(tmp_path): @pytest.mark.skipif( - sys.platform == "linux" and not _c_internal_utils.display_is_valid(), + sys.platform == "linux" and not _c_internal_utils.xdisplay_is_valid(), reason="headless") def test_backend_fallback_headful(tmp_path): - pytest.importorskip("tkinter") + if parse_version(pytest.__version__) >= parse_version('8.2.0'): + pytest_kwargs = dict(exc_type=ImportError) + else: + pytest_kwargs = {} + + pytest.importorskip("tkinter", **pytest_kwargs) env = {**os.environ, "MPLBACKEND": "", "MPLCONFIGDIR": str(tmp_path)} backend = subprocess_run_for_testing( [sys.executable, "-c", diff --git a/lib/matplotlib/tests/test_table.py b/lib/matplotlib/tests/test_table.py index ea31ac124e4a..653e918eecc8 100644 --- a/lib/matplotlib/tests/test_table.py +++ b/lib/matplotlib/tests/test_table.py @@ -2,10 +2,8 @@ from unittest.mock import Mock import numpy as np -import pytest import matplotlib.pyplot as plt -import matplotlib as mpl from matplotlib.path import Path from matplotlib.table import CustomCell, Table from matplotlib.testing.decorators import image_comparison, check_figures_equal @@ -128,10 +126,9 @@ def test_customcell(): @image_comparison(['table_auto_column.png']) def test_auto_column(): - fig = plt.figure() + fig, (ax1, ax2, ax3, ax4) = plt.subplots(4, 1) # iterable list input - ax1 = fig.add_subplot(4, 1, 1) ax1.axis('off') tb1 = ax1.table( cellText=[['Fit Text', 2], @@ -144,7 +141,6 @@ def test_auto_column(): tb1.auto_set_column_width([-1, 0, 1]) # iterable tuple input - ax2 = fig.add_subplot(4, 1, 2) ax2.axis('off') tb2 = ax2.table( cellText=[['Fit Text', 2], @@ -157,7 +153,6 @@ def test_auto_column(): tb2.auto_set_column_width((-1, 0, 1)) # 3 single inputs - ax3 = fig.add_subplot(4, 1, 3) ax3.axis('off') tb3 = ax3.table( cellText=[['Fit Text', 2], @@ -171,8 +166,8 @@ def test_auto_column(): tb3.auto_set_column_width(0) tb3.auto_set_column_width(1) - # 4 non integer iterable input - ax4 = fig.add_subplot(4, 1, 4) + # 4 this used to test non-integer iterable input, which did nothing, but only + # remains to avoid re-generating the test image. ax4.axis('off') tb4 = ax4.table( cellText=[['Fit Text', 2], @@ -182,12 +177,6 @@ def test_auto_column(): loc="center") tb4.auto_set_font_size(False) tb4.set_fontsize(12) - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="'col' must be an int or sequence of ints"): - tb4.auto_set_column_width("-101") # type: ignore [arg-type] - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="'col' must be an int or sequence of ints"): - tb4.auto_set_column_width(["-101"]) # type: ignore [list-item] def test_table_cells(): diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 2ac8716a2b4d..585d846944e8 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -3,7 +3,6 @@ import operator from unittest import mock -import matplotlib as mpl from matplotlib.backend_bases import MouseEvent import matplotlib.colors as mcolors import matplotlib.widgets as widgets @@ -71,7 +70,7 @@ def test_save_blitted_widget_as_pdf(): def test_rectangle_selector(ax, kwargs): onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.RectangleSelector(ax, onselect, **kwargs) + tool = widgets.RectangleSelector(ax, onselect=onselect, **kwargs) do_event(tool, 'press', xdata=100, ydata=100, button=1) do_event(tool, 'onmove', xdata=199, ydata=199, button=1) @@ -105,7 +104,7 @@ def test_rectangle_minspan(ax, spancoords, minspanx, x1, minspany, y1): minspanx, minspany = (ax.transData.transform((x1, y1)) - ax.transData.transform((x0, y0))) - tool = widgets.RectangleSelector(ax, onselect, interactive=True, + tool = widgets.RectangleSelector(ax, onselect=onselect, interactive=True, spancoords=spancoords, minspanx=minspanx, minspany=minspany) # Too small to create a selector @@ -131,21 +130,11 @@ def test_rectangle_minspan(ax, spancoords, minspanx, x1, minspany, y1): assert kwargs == {} -def test_deprecation_selector_visible_attribute(ax): - tool = widgets.RectangleSelector(ax, lambda *args: None) - - assert tool.get_visible() - - with pytest.warns(mpl.MatplotlibDeprecationWarning, - match="was deprecated in Matplotlib 3.8"): - tool.visible - - @pytest.mark.parametrize('drag_from_anywhere, new_center', [[True, (60, 75)], [False, (30, 20)]]) def test_rectangle_drag(ax, drag_from_anywhere, new_center): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True, + tool = widgets.RectangleSelector(ax, interactive=True, drag_from_anywhere=drag_from_anywhere) # Create rectangle click_and_drag(tool, start=(0, 10), end=(100, 120)) @@ -166,7 +155,7 @@ def test_rectangle_drag(ax, drag_from_anywhere, new_center): def test_rectangle_selector_set_props_handle_props(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True, + tool = widgets.RectangleSelector(ax, interactive=True, props=dict(facecolor='b', alpha=0.2), handle_props=dict(alpha=0.5)) # Create rectangle @@ -187,7 +176,7 @@ def test_rectangle_selector_set_props_handle_props(ax): def test_rectangle_resize(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Create rectangle click_and_drag(tool, start=(0, 10), end=(100, 120)) assert tool.extents == (0.0, 100.0, 10.0, 120.0) @@ -222,7 +211,7 @@ def test_rectangle_resize(ax): def test_rectangle_add_state(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Create rectangle click_and_drag(tool, start=(70, 65), end=(125, 130)) @@ -238,7 +227,7 @@ def test_rectangle_add_state(ax): @pytest.mark.parametrize('add_state', [True, False]) def test_rectangle_resize_center(ax, add_state): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Create rectangle click_and_drag(tool, start=(70, 65), end=(125, 130)) assert tool.extents == (70.0, 125.0, 65.0, 130.0) @@ -312,7 +301,7 @@ def test_rectangle_resize_center(ax, add_state): @pytest.mark.parametrize('add_state', [True, False]) def test_rectangle_resize_square(ax, add_state): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Create rectangle click_and_drag(tool, start=(70, 65), end=(120, 115)) assert tool.extents == (70.0, 120.0, 65.0, 115.0) @@ -385,7 +374,7 @@ def test_rectangle_resize_square(ax, add_state): def test_rectangle_resize_square_center(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Create rectangle click_and_drag(tool, start=(70, 65), end=(120, 115)) tool.add_state('square') @@ -450,7 +439,7 @@ def test_rectangle_resize_square_center(ax): @pytest.mark.parametrize('selector_class', [widgets.RectangleSelector, widgets.EllipseSelector]) def test_rectangle_rotate(ax, selector_class): - tool = selector_class(ax, onselect=noop, interactive=True) + tool = selector_class(ax, interactive=True) # Draw rectangle click_and_drag(tool, start=(100, 100), end=(130, 140)) assert tool.extents == (100, 130, 100, 140) @@ -483,7 +472,7 @@ def test_rectangle_rotate(ax, selector_class): def test_rectangle_add_remove_set(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) # Draw rectangle click_and_drag(tool, start=(100, 100), end=(130, 140)) assert tool.extents == (100, 130, 100, 140) @@ -499,7 +488,7 @@ def test_rectangle_add_remove_set(ax): def test_rectangle_resize_square_center_aspect(ax, use_data_coordinates): ax.set_aspect(0.8) - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True, + tool = widgets.RectangleSelector(ax, interactive=True, use_data_coordinates=use_data_coordinates) # Create rectangle click_and_drag(tool, start=(70, 65), end=(120, 115)) @@ -531,8 +520,7 @@ def test_rectangle_resize_square_center_aspect(ax, use_data_coordinates): def test_ellipse(ax): """For ellipse, test out the key modifiers""" - tool = widgets.EllipseSelector(ax, onselect=noop, - grab_range=10, interactive=True) + tool = widgets.EllipseSelector(ax, grab_range=10, interactive=True) tool.extents = (100, 150, 100, 150) # drag the rectangle @@ -558,9 +546,7 @@ def test_ellipse(ax): def test_rectangle_handles(ax): - tool = widgets.RectangleSelector(ax, onselect=noop, - grab_range=10, - interactive=True, + tool = widgets.RectangleSelector(ax, grab_range=10, interactive=True, handle_props={'markerfacecolor': 'r', 'markeredgecolor': 'b'}) tool.extents = (100, 150, 100, 150) @@ -595,7 +581,7 @@ def test_rectangle_selector_onselect(ax, interactive): # check when press and release events take place at the same position onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.RectangleSelector(ax, onselect, interactive=interactive) + tool = widgets.RectangleSelector(ax, onselect=onselect, interactive=interactive) # move outside of axis click_and_drag(tool, start=(100, 110), end=(150, 120)) @@ -611,7 +597,7 @@ def test_rectangle_selector_onselect(ax, interactive): def test_rectangle_selector_ignore_outside(ax, ignore_event_outside): onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.RectangleSelector(ax, onselect, + tool = widgets.RectangleSelector(ax, onselect=onselect, ignore_event_outside=ignore_event_outside) click_and_drag(tool, start=(100, 110), end=(150, 120)) onselect.assert_called_once() @@ -773,10 +759,11 @@ def test_span_selector_set_props_handle_props(ax): @pytest.mark.parametrize('selector', ['span', 'rectangle']) def test_selector_clear(ax, selector): - kwargs = dict(ax=ax, onselect=noop, interactive=True) + kwargs = dict(ax=ax, interactive=True) if selector == 'span': Selector = widgets.SpanSelector kwargs['direction'] = 'horizontal' + kwargs['onselect'] = noop else: Selector = widgets.RectangleSelector @@ -807,7 +794,7 @@ def test_selector_clear_method(ax, selector): interactive=True, ignore_event_outside=True) else: - tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + tool = widgets.RectangleSelector(ax, interactive=True) click_and_drag(tool, start=(10, 10), end=(100, 120)) assert tool._selection_completed assert tool.get_visible() @@ -1000,7 +987,7 @@ def test_span_selector_extents(ax): def test_lasso_selector(ax, kwargs): onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.LassoSelector(ax, onselect, **kwargs) + tool = widgets.LassoSelector(ax, onselect=onselect, **kwargs) do_event(tool, 'press', xdata=100, ydata=100, button=1) do_event(tool, 'onmove', xdata=125, ydata=125, button=1) do_event(tool, 'release', xdata=150, ydata=150, button=1) @@ -1011,7 +998,8 @@ def test_lasso_selector(ax, kwargs): def test_lasso_selector_set_props(ax): onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.LassoSelector(ax, onselect, props=dict(color='b', alpha=0.2)) + tool = widgets.LassoSelector(ax, onselect=onselect, + props=dict(color='b', alpha=0.2)) artist = tool._selection_artist assert mcolors.same_color(artist.get_color(), 'b') @@ -1380,7 +1368,7 @@ def check_polygon_selector(event_sequence, expected_result, selections_count, onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.PolygonSelector(ax, onselect, **kwargs) + tool = widgets.PolygonSelector(ax, onselect=onselect, **kwargs) for (etype, event_args) in event_sequence: do_event(tool, etype, **event_args) @@ -1517,7 +1505,7 @@ def test_polygon_selector(draw_bounding_box): @pytest.mark.parametrize('draw_bounding_box', [False, True]) def test_polygon_selector_set_props_handle_props(ax, draw_bounding_box): - tool = widgets.PolygonSelector(ax, onselect=noop, + tool = widgets.PolygonSelector(ax, props=dict(color='b', alpha=0.2), handle_props=dict(alpha=0.5), draw_bounding_box=draw_bounding_box) @@ -1554,8 +1542,7 @@ def test_rect_visibility(fig_test, fig_ref): ax_test = fig_test.subplots() _ = fig_ref.subplots() - tool = widgets.RectangleSelector(ax_test, onselect=noop, - props={'visible': False}) + tool = widgets.RectangleSelector(ax_test, props={'visible': False}) tool.extents = (0.2, 0.8, 0.3, 0.7) @@ -1608,8 +1595,7 @@ def test_polygon_selector_redraw(ax, draw_bounding_box): *polygon_place_vertex(*verts[1]), ] - tool = widgets.PolygonSelector(ax, onselect=noop, - draw_bounding_box=draw_bounding_box) + tool = widgets.PolygonSelector(ax, draw_bounding_box=draw_bounding_box) for (etype, event_args) in event_sequence: do_event(tool, etype, **event_args) # After removing two verts, only one remains, and the @@ -1623,14 +1609,12 @@ def test_polygon_selector_verts_setter(fig_test, fig_ref, draw_bounding_box): verts = [(0.1, 0.4), (0.5, 0.9), (0.3, 0.2)] ax_test = fig_test.add_subplot() - tool_test = widgets.PolygonSelector( - ax_test, onselect=noop, draw_bounding_box=draw_bounding_box) + tool_test = widgets.PolygonSelector(ax_test, draw_bounding_box=draw_bounding_box) tool_test.verts = verts assert tool_test.verts == verts ax_ref = fig_ref.add_subplot() - tool_ref = widgets.PolygonSelector( - ax_ref, onselect=noop, draw_bounding_box=draw_bounding_box) + tool_ref = widgets.PolygonSelector(ax_ref, draw_bounding_box=draw_bounding_box) event_sequence = [ *polygon_place_vertex(*verts[0]), *polygon_place_vertex(*verts[1]), @@ -1654,7 +1638,7 @@ def test_polygon_selector_box(ax): ] # Create selector - tool = widgets.PolygonSelector(ax, onselect=noop, draw_bounding_box=True) + tool = widgets.PolygonSelector(ax, draw_bounding_box=True) for (etype, event_args) in event_sequence: do_event(tool, etype, **event_args) diff --git a/lib/matplotlib/texmanager.py b/lib/matplotlib/texmanager.py index 8deb03b3e148..a374bfba8cab 100644 --- a/lib/matplotlib/texmanager.py +++ b/lib/matplotlib/texmanager.py @@ -31,7 +31,7 @@ import numpy as np import matplotlib as mpl -from matplotlib import _api, cbook, dviread +from matplotlib import cbook, dviread _log = logging.getLogger(__name__) @@ -63,7 +63,6 @@ class TexManager: Repeated calls to this constructor always return the same instance. """ - texcache = _api.deprecate_privatize_attribute("3.8") _texcache = os.path.join(mpl.get_cachedir(), 'tex.cache') _grey_arrayd = {} diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index 6691f32f8771..0b65450f760b 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -754,9 +754,16 @@ def draw(self, renderer): # don't use self.get_position here, which refers to text # position in Text: - posx = float(self.convert_xunits(self._x)) - posy = float(self.convert_yunits(self._y)) + x, y = self._x, self._y + if np.ma.is_masked(x): + x = np.nan + if np.ma.is_masked(y): + y = np.nan + posx = float(self.convert_xunits(x)) + posy = float(self.convert_yunits(y)) posx, posy = trans.transform((posx, posy)) + if np.isnan(posx) or np.isnan(posy): + return # don't throw a warning here if not np.isfinite(posx) or not np.isfinite(posy): _log.warning("posx and posy should be finite values") return @@ -1842,10 +1849,6 @@ def transform(renderer) -> Transform # modified YAArrow API to be used with FancyArrowPatch for key in ['width', 'headwidth', 'headlength', 'shrink']: arrowprops.pop(key, None) - if 'frac' in arrowprops: - _api.warn_deprecated( - "3.8", name="the (unused) 'frac' key in 'arrowprops'") - arrowprops.pop("frac") self.arrow_patch = FancyArrowPatch((0, 0), (1, 1), **arrowprops) else: self.arrow_patch = None @@ -2029,4 +2032,4 @@ def get_tightbbox(self, renderer=None): return super().get_tightbbox(renderer) -_docstring.interpd.update(Annotation=Annotation.__init__.__doc__) +_docstring.interpd.register(Annotation=Annotation.__init__.__doc__) diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 23abb6aef6b9..0053031ece3e 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -574,7 +574,7 @@ def set_useMathText(self, val): from matplotlib import font_manager ufont = font_manager.findfont( font_manager.FontProperties( - mpl.rcParams["font.family"] + family=mpl.rcParams["font.family"] ), fallback_to_default=False, ) diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index fde6d6732171..bc7ce8b9e965 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -49,6 +49,7 @@ from matplotlib import _api from matplotlib._path import ( affine_transform, count_bboxes_overlapping_bbox, update_path_extents) +from matplotlib import transforms as mtransforms from .path import Path DEBUG = False @@ -93,9 +94,6 @@ class TransformNode: # Invalidation may affect only the affine part. If the # invalidation was "affine-only", the _invalid member is set to # INVALID_AFFINE_ONLY - INVALID_NON_AFFINE = _api.deprecated("3.8")(_api.classproperty(lambda cls: 1)) - INVALID_AFFINE = _api.deprecated("3.8")(_api.classproperty(lambda cls: 2)) - INVALID = _api.deprecated("3.8")(_api.classproperty(lambda cls: 3)) # Possible values for the _invalid attribute. _VALID, _INVALID_AFFINE_ONLY, _INVALID_FULL = range(3) @@ -480,7 +478,7 @@ def transformed(self, transform): 'NW': (0, 1.0), 'W': (0, 0.5)} - def anchored(self, c, container=None): + def anchored(self, c, container): """ Return a copy of the `Bbox` anchored to *c* within *container*. @@ -490,19 +488,13 @@ def anchored(self, c, container=None): Either an (*x*, *y*) pair of relative coordinates (0 is left or bottom, 1 is right or top), 'C' (center), or a cardinal direction ('SW', southwest, is bottom left, etc.). - container : `Bbox`, optional + container : `Bbox` The box within which the `Bbox` is positioned. See Also -------- .Axes.set_anchor """ - if container is None: - _api.warn_deprecated( - "3.8", message="Calling anchored() with no container bbox " - "returns a frozen copy of the original bbox and is deprecated " - "since %(since)s.") - container = self l, b, w, h = container.bounds L, B, W, H = self.bounds cx, cy = self.coefs[c] if isinstance(c, str) else c @@ -2984,3 +2976,28 @@ def offset_copy(trans, fig=None, x=0.0, y=0.0, units='inches'): y /= 72.0 # Default units are 'inches' return trans + ScaledTranslation(x, y, fig.dpi_scale_trans) + + +class ScaledRotation(Affine2DBase): + """ + A transformation that applies offset and direction + based on *trans_shift*. + """ + def __init__(self, theta, trans_shift, is_radial): + super().__init__() + self._theta = theta + self._trans_shift = trans_shift + self._is_radial = is_radial + self._mtx = np.identity(3) + self._invalid = True + + def get_matrix(self): + if self._invalid: + transformed_coords = self._trans_shift.transform([[self._theta, 0]])[0] + adjusted_theta = transformed_coords[0] + if self._is_radial: + adjusted_theta -= np.pi / 2 + rotation = mtransforms.Affine2D().rotate(adjusted_theta) + self._mtx[:2, :2] = rotation.get_matrix()[:2, :2] + self._invalid = False + return self._mtx diff --git a/lib/matplotlib/transforms.pyi b/lib/matplotlib/transforms.pyi index 90a527e5bfc5..888ad7ff82cf 100644 --- a/lib/matplotlib/transforms.pyi +++ b/lib/matplotlib/transforms.pyi @@ -77,9 +77,10 @@ class BboxBase(TransformNode): def fully_overlaps(self, other: BboxBase) -> bool: ... def transformed(self, transform: Transform) -> Bbox: ... coefs: dict[str, tuple[float, float]] - # anchored type can be s/str/Literal["C", "SW", "S", "SE", "E", "NE", "N", "NW", "W"] def anchored( - self, c: tuple[float, float] | str, container: BboxBase | None = ... + self, + c: tuple[float, float] | Literal['C', 'SW', 'S', 'SE', 'E', 'NE', 'N', 'NW', 'W'], + container: BboxBase, ) -> Bbox: ... def shrunk(self, mx: float, my: float) -> Bbox: ... def shrunk_to_aspect( @@ -333,3 +334,7 @@ def offset_copy( y: float = ..., units: Literal["inches", "points", "dots"] = ..., ) -> Transform: ... + +class ScaledRotation(Affine2DBase): + def __init__(self, theta: float, trans_shift: Transform, is_radial: bool) -> None: ... + def get_matrix(self) -> np.ndarray: ... diff --git a/lib/matplotlib/tri/_tricontour.py b/lib/matplotlib/tri/_tricontour.py index c09d04f9e543..8250515f3ef8 100644 --- a/lib/matplotlib/tri/_tricontour.py +++ b/lib/matplotlib/tri/_tricontour.py @@ -79,7 +79,7 @@ def _contour_args(self, args, kwargs): return (tri, z) -_docstring.interpd.update(_tricontour_doc=""" +_docstring.interpd.register(_tricontour_doc=""" Draw contour %%(type)s on an unstructured triangular grid. Call signatures:: diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 4245ce665b00..9c676574310c 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -2091,12 +2091,15 @@ def onmove(self, event): class _SelectorWidget(AxesWidget): - def __init__(self, ax, onselect, useblit=False, button=None, + def __init__(self, ax, onselect=None, useblit=False, button=None, state_modifier_keys=None, use_data_coordinates=False): super().__init__(ax) self._visible = True - self.onselect = onselect + if onselect is None: + self.onselect = lambda *args: None + else: + self.onselect = onselect self.useblit = useblit and self.canvas.supports_blit self.connect_default_events() @@ -2347,11 +2350,6 @@ def get_visible(self): """Get the visibility of the selector artists.""" return self._visible - @property - def visible(self): - _api.warn_deprecated("3.8", alternative="get_visible") - return self.get_visible() - def clear(self): """Clear the selection and set the selector ready to make a new one.""" self._clear_without_update() @@ -3041,7 +3039,7 @@ def closest(self, x, y): ax : `~matplotlib.axes.Axes` The parent Axes for the widget. - onselect : function + onselect : function, optional A callback function that is called after a release event and the selection is created, changed or removed. It must have the signature:: @@ -3154,7 +3152,8 @@ class RectangleSelector(_SelectorWidget): See also: :doc:`/gallery/widgets/rectangle_selector` """ - def __init__(self, ax, onselect, *, minspanx=0, minspany=0, useblit=False, + def __init__(self, ax, onselect=None, *, minspanx=0, + minspany=0, useblit=False, props=None, spancoords='data', button=None, grab_range=10, handle_props=None, interactive=False, state_modifier_keys=None, drag_from_anywhere=False, @@ -3676,7 +3675,7 @@ def onselect(verts): ---------- ax : `~matplotlib.axes.Axes` The parent Axes for the widget. - onselect : function + onselect : function, optional Whenever the lasso is released, the *onselect* function is called and passed the vertices of the selected path. useblit : bool, default: True @@ -3691,7 +3690,7 @@ def onselect(verts): which corresponds to all buttons. """ - def __init__(self, ax, onselect, *, useblit=True, props=None, button=None): + def __init__(self, ax, onselect=None, *, useblit=True, props=None, button=None): super().__init__(ax, onselect, useblit=useblit, button=button) self.verts = None props = { @@ -3749,7 +3748,7 @@ class PolygonSelector(_SelectorWidget): ax : `~matplotlib.axes.Axes` The parent Axes for the widget. - onselect : function + onselect : function, optional When a polygon is completed or modified after completion, the *onselect* function is called and passed a list of the vertices as ``(xdata, ydata)`` tuples. @@ -3801,7 +3800,7 @@ class PolygonSelector(_SelectorWidget): point. """ - def __init__(self, ax, onselect, *, useblit=False, + def __init__(self, ax, onselect=None, *, useblit=False, props=None, handle_props=None, grab_range=10, draw_bounding_box=False, box_handle_props=None, box_props=None): @@ -3851,7 +3850,6 @@ def _get_bbox(self): def _add_box(self): self._box = RectangleSelector(self.ax, - onselect=lambda *args, **kwargs: None, useblit=self.useblit, grab_range=self.grab_range, handle_props=self._box_handle_props, diff --git a/lib/matplotlib/widgets.pyi b/lib/matplotlib/widgets.pyi index f5de6cb62414..0fcd1990e17e 100644 --- a/lib/matplotlib/widgets.pyi +++ b/lib/matplotlib/widgets.pyi @@ -276,7 +276,7 @@ class _SelectorWidget(AxesWidget): def __init__( self, ax: Axes, - onselect: Callable[[float, float], Any], + onselect: Callable[[float, float], Any] | None = ..., useblit: bool = ..., button: MouseButton | Collection[MouseButton] | None = ..., state_modifier_keys: dict[str, str] | None = ..., @@ -294,8 +294,6 @@ class _SelectorWidget(AxesWidget): def on_key_release(self, event: Event) -> None: ... def set_visible(self, visible: bool) -> None: ... def get_visible(self) -> bool: ... - @property - def visible(self) -> bool: ... def clear(self) -> None: ... @property def artists(self) -> tuple[Artist]: ... @@ -403,7 +401,7 @@ class RectangleSelector(_SelectorWidget): def __init__( self, ax: Axes, - onselect: Callable[[MouseEvent, MouseEvent], Any], + onselect: Callable[[MouseEvent, MouseEvent], Any] | None = ..., *, minspanx: float = ..., minspany: float = ..., @@ -443,7 +441,7 @@ class LassoSelector(_SelectorWidget): def __init__( self, ax: Axes, - onselect: Callable[[list[tuple[float, float]]], Any], + onselect: Callable[[list[tuple[float, float]]], Any] | None = ..., *, useblit: bool = ..., props: dict[str, Any] | None = ..., @@ -455,7 +453,7 @@ class PolygonSelector(_SelectorWidget): def __init__( self, ax: Axes, - onselect: Callable[[ArrayLike, ArrayLike], Any], + onselect: Callable[[ArrayLike, ArrayLike], Any] | None = ..., *, useblit: bool = ..., props: dict[str, Any] | None = ..., diff --git a/lib/mpl_toolkits/axes_grid1/anchored_artists.py b/lib/mpl_toolkits/axes_grid1/anchored_artists.py index 1238310b462b..214b15843ebf 100644 --- a/lib/mpl_toolkits/axes_grid1/anchored_artists.py +++ b/lib/mpl_toolkits/axes_grid1/anchored_artists.py @@ -1,12 +1,12 @@ -from matplotlib import _api, transforms +from matplotlib import transforms from matplotlib.offsetbox import (AnchoredOffsetbox, AuxTransformBox, DrawingArea, TextArea, VPacker) -from matplotlib.patches import (Rectangle, Ellipse, ArrowStyle, +from matplotlib.patches import (Rectangle, ArrowStyle, FancyArrowPatch, PathPatch) from matplotlib.text import TextPath __all__ = ['AnchoredDrawingArea', 'AnchoredAuxTransformBox', - 'AnchoredEllipse', 'AnchoredSizeBar', 'AnchoredDirectionArrows'] + 'AnchoredSizeBar', 'AnchoredDirectionArrows'] class AnchoredDrawingArea(AnchoredOffsetbox): @@ -124,54 +124,6 @@ def __init__(self, transform, loc, **kwargs) -@_api.deprecated("3.8") -class AnchoredEllipse(AnchoredOffsetbox): - def __init__(self, transform, width, height, angle, loc, - pad=0.1, borderpad=0.1, prop=None, frameon=True, **kwargs): - """ - Draw an anchored ellipse of a given size. - - Parameters - ---------- - transform : `~matplotlib.transforms.Transform` - The transformation object for the coordinate system in use, i.e., - :attr:`matplotlib.axes.Axes.transData`. - width, height : float - Width and height of the ellipse, given in coordinates of - *transform*. - angle : float - Rotation of the ellipse, in degrees, anti-clockwise. - loc : str - Location of the ellipse. Valid locations are - 'upper left', 'upper center', 'upper right', - 'center left', 'center', 'center right', - 'lower left', 'lower center', 'lower right'. - For backward compatibility, numeric values are accepted as well. - See the parameter *loc* of `.Legend` for details. - pad : float, default: 0.1 - Padding around the ellipse, in fraction of the font size. - borderpad : float, default: 0.1 - Border padding, in fraction of the font size. - frameon : bool, default: True - If True, draw a box around the ellipse. - prop : `~matplotlib.font_manager.FontProperties`, optional - Font property used as a reference for paddings. - **kwargs - Keyword arguments forwarded to `.AnchoredOffsetbox`. - - Attributes - ---------- - ellipse : `~matplotlib.patches.Ellipse` - Ellipse patch drawn. - """ - self._box = AuxTransformBox(transform) - self.ellipse = Ellipse((0, 0), width, height, angle=angle) - self._box.add_artist(self.ellipse) - - super().__init__(loc, pad=pad, borderpad=borderpad, child=self._box, - prop=prop, frameon=frameon, **kwargs) - - class AnchoredSizeBar(AnchoredOffsetbox): def __init__(self, transform, size, label, loc, pad=0.1, borderpad=0.1, sep=2, diff --git a/lib/mpl_toolkits/axes_grid1/axes_divider.py b/lib/mpl_toolkits/axes_grid1/axes_divider.py index f6c38f35dbc4..50365f482b72 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_divider.py +++ b/lib/mpl_toolkits/axes_grid1/axes_divider.py @@ -199,31 +199,6 @@ def new_locator(self, nx, ny, nx1=None, ny1=None): locator.get_subplotspec = self.get_subplotspec return locator - @_api.deprecated( - "3.8", alternative="divider.new_locator(...)(ax, renderer)") - def locate(self, nx, ny, nx1=None, ny1=None, axes=None, renderer=None): - """ - Implementation of ``divider.new_locator().__call__``. - - Parameters - ---------- - nx, nx1 : int - Integers specifying the column-position of the cell. When *nx1* is - None, a single *nx*-th column is specified. Otherwise, the - location of columns spanning between *nx* to *nx1* (but excluding - *nx1*-th column) is specified. - ny, ny1 : int - Same as *nx* and *nx1*, but for row positions. - axes - renderer - """ - xref = self._xrefindex - yref = self._yrefindex - return self._locate( - nx - xref, (nx + 1 if nx1 is None else nx1) - xref, - ny - yref, (ny + 1 if ny1 is None else ny1) - yref, - axes, renderer) - def _locate(self, nx, ny, nx1, ny1, axes, renderer): """ Implementation of ``divider.new_locator().__call__``. @@ -305,57 +280,6 @@ def add_auto_adjustable_area(self, use_axes, pad=0.1, adjust_dirs=None): self.append_size(d, Size._AxesDecorationsSize(use_axes, d) + pad) -@_api.deprecated("3.8") -class AxesLocator: - """ - A callable object which returns the position and size of a given - `.AxesDivider` cell. - """ - - def __init__(self, axes_divider, nx, ny, nx1=None, ny1=None): - """ - Parameters - ---------- - axes_divider : `~mpl_toolkits.axes_grid1.axes_divider.AxesDivider` - nx, nx1 : int - Integers specifying the column-position of the - cell. When *nx1* is None, a single *nx*-th column is - specified. Otherwise, location of columns spanning between *nx* - to *nx1* (but excluding *nx1*-th column) is specified. - ny, ny1 : int - Same as *nx* and *nx1*, but for row positions. - """ - self._axes_divider = axes_divider - - _xrefindex = axes_divider._xrefindex - _yrefindex = axes_divider._yrefindex - - self._nx, self._ny = nx - _xrefindex, ny - _yrefindex - - if nx1 is None: - nx1 = len(self._axes_divider) - if ny1 is None: - ny1 = len(self._axes_divider[0]) - - self._nx1 = nx1 - _xrefindex - self._ny1 = ny1 - _yrefindex - - def __call__(self, axes, renderer): - - _xrefindex = self._axes_divider._xrefindex - _yrefindex = self._axes_divider._yrefindex - - return self._axes_divider.locate(self._nx + _xrefindex, - self._ny + _yrefindex, - self._nx1 + _xrefindex, - self._ny1 + _yrefindex, - axes, - renderer) - - def get_subplotspec(self): - return self._axes_divider.get_subplotspec() - - class SubplotDivider(Divider): """ The Divider class whose rectangle area is specified as a subplot geometry. diff --git a/lib/mpl_toolkits/axes_grid1/axes_grid.py b/lib/mpl_toolkits/axes_grid1/axes_grid.py index b5663364481e..20abf18ea79c 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_grid.py +++ b/lib/mpl_toolkits/axes_grid1/axes_grid.py @@ -20,11 +20,6 @@ def colorbar(self, mappable, **kwargs): return self.get_figure(root=False).colorbar( mappable, cax=self, location=self.orientation, **kwargs) - @_api.deprecated("3.8", alternative="ax.tick_params and colorbar.set_label") - def toggle_label(self, b): - axis = self.axis[self.orientation] - axis.toggle(ticklabels=b, label=b) - _cbaraxes_class_factory = cbook._make_class_factory(CbarAxesBase, "Cbar{}") diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py index 303dbbb0721e..52fe6efc0618 100644 --- a/lib/mpl_toolkits/axes_grid1/inset_locator.py +++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py @@ -6,58 +6,13 @@ from matplotlib.offsetbox import AnchoredOffsetbox from matplotlib.patches import Patch, Rectangle from matplotlib.path import Path -from matplotlib.transforms import Bbox, BboxTransformTo +from matplotlib.transforms import Bbox from matplotlib.transforms import IdentityTransform, TransformedBbox from . import axes_size as Size from .parasite_axes import HostAxes -@_api.deprecated("3.8", alternative="Axes.inset_axes") -class InsetPosition: - @_docstring.interpd - def __init__(self, parent, lbwh): - """ - An object for positioning an inset axes. - - This is created by specifying the normalized coordinates in the axes, - instead of the figure. - - Parameters - ---------- - parent : `~matplotlib.axes.Axes` - Axes to use for normalizing coordinates. - - lbwh : iterable of four floats - The left edge, bottom edge, width, and height of the inset axes, in - units of the normalized coordinate of the *parent* axes. - - See Also - -------- - :meth:`matplotlib.axes.Axes.set_axes_locator` - - Examples - -------- - The following bounds the inset axes to a box with 20%% of the parent - axes height and 40%% of the width. The size of the axes specified - ([0, 0, 1, 1]) ensures that the axes completely fills the bounding box: - - >>> parent_axes = plt.gca() - >>> ax_ins = plt.axes([0, 0, 1, 1]) - >>> ip = InsetPosition(parent_axes, [0.5, 0.1, 0.4, 0.2]) - >>> ax_ins.set_axes_locator(ip) - """ - self.parent = parent - self.lbwh = lbwh - - def __call__(self, ax, renderer): - bbox_parent = self.parent.get_position(original=False) - trans = BboxTransformTo(bbox_parent) - bbox_inset = Bbox.from_bounds(*self.lbwh) - bb = TransformedBbox(bbox_inset, trans) - return bb - - class AnchoredLocatorBase(AnchoredOffsetbox): def __init__(self, bbox_to_anchor, offsetbox, loc, borderpad=0.5, bbox_transform=None): diff --git a/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/insetposition.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/insetposition.png deleted file mode 100644 index e8676cfd6c95..000000000000 Binary files a/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/insetposition.png and /dev/null differ diff --git a/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py b/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py index 346fcc1d8f02..778bd9ca04d0 100644 --- a/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py +++ b/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py @@ -18,15 +18,14 @@ host_subplot, make_axes_locatable, Grid, AxesGrid, ImageGrid) from mpl_toolkits.axes_grid1.anchored_artists import ( - AnchoredAuxTransformBox, AnchoredDrawingArea, AnchoredEllipse, + AnchoredAuxTransformBox, AnchoredDrawingArea, AnchoredDirectionArrows, AnchoredSizeBar) from mpl_toolkits.axes_grid1.axes_divider import ( Divider, HBoxDivider, make_axes_area_auto_adjustable, SubplotDivider, VBoxDivider) from mpl_toolkits.axes_grid1.axes_rgb import RGBAxes from mpl_toolkits.axes_grid1.inset_locator import ( - zoomed_inset_axes, mark_inset, inset_axes, BboxConnectorPatch, - InsetPosition) + zoomed_inset_axes, mark_inset, inset_axes, BboxConnectorPatch) import mpl_toolkits.axes_grid1.mpl_axes import pytest @@ -543,12 +542,14 @@ def test_anchored_artists(): box.drawing_area.add_artist(el) ax.add_artist(box) - # Manually construct the ellipse instead, once the deprecation elapses. - with pytest.warns(mpl.MatplotlibDeprecationWarning): - ae = AnchoredEllipse(ax.transData, width=0.1, height=0.25, angle=-60, - loc='lower left', pad=0.5, borderpad=0.4, - frameon=True) - ax.add_artist(ae) + # This block used to test the AnchoredEllipse class, but that was removed. The block + # remains, though it duplicates the above ellipse, so that the test image doesn't + # need to be regenerated. + box = AnchoredAuxTransformBox(ax.transData, loc='lower left', frameon=True, + pad=0.5, borderpad=0.4) + el = Ellipse((0, 0), width=0.1, height=0.25, angle=-60) + box.drawing_area.add_artist(el) + ax.add_artist(box) asb = AnchoredSizeBar(ax.transData, 0.2, r"0.2 units", loc='lower right', pad=0.3, borderpad=0.4, sep=4, fill_bar=True, @@ -702,17 +703,6 @@ def test_rgb_axes(): ax.imshow_rgb(r, g, b, interpolation='none') -# Update style when regenerating the test image -@image_comparison(['insetposition.png'], remove_text=True, - style=('classic', '_classic_test_patch')) -def test_insetposition(): - fig, ax = plt.subplots(figsize=(2, 2)) - ax_ins = plt.axes([0, 0, 1, 1]) - with pytest.warns(mpl.MatplotlibDeprecationWarning): - ip = InsetPosition(ax, [0.2, 0.25, 0.5, 0.4]) - ax_ins.set_axes_locator(ip) - - # The original version of this test relied on mpl_toolkits's slightly different # colorbar implementation; moving to matplotlib's own colorbar implementation # caused the small image comparison error. diff --git a/lib/mpl_toolkits/axisartist/axes_divider.py b/lib/mpl_toolkits/axisartist/axes_divider.py index a01d4e27df93..d0392be782d9 100644 --- a/lib/mpl_toolkits/axisartist/axes_divider.py +++ b/lib/mpl_toolkits/axisartist/axes_divider.py @@ -1,2 +1,2 @@ from mpl_toolkits.axes_grid1.axes_divider import ( # noqa - Divider, AxesLocator, SubplotDivider, AxesDivider, make_axes_locatable) + Divider, SubplotDivider, AxesDivider, make_axes_locatable) diff --git a/lib/mpl_toolkits/axisartist/axes_grid.py b/lib/mpl_toolkits/axisartist/axes_grid.py deleted file mode 100644 index ecb3e9d92c18..000000000000 --- a/lib/mpl_toolkits/axisartist/axes_grid.py +++ /dev/null @@ -1,23 +0,0 @@ -from matplotlib import _api - -import mpl_toolkits.axes_grid1.axes_grid as axes_grid_orig -from .axislines import Axes - - -_api.warn_deprecated( - "3.8", name=__name__, obj_type="module", alternative="axes_grid1.axes_grid") - - -@_api.deprecated("3.8", alternative=( - "axes_grid1.axes_grid.Grid(..., axes_class=axislines.Axes")) -class Grid(axes_grid_orig.Grid): - _defaultAxesClass = Axes - - -@_api.deprecated("3.8", alternative=( - "axes_grid1.axes_grid.ImageGrid(..., axes_class=axislines.Axes")) -class ImageGrid(axes_grid_orig.ImageGrid): - _defaultAxesClass = Axes - - -AxesGrid = ImageGrid diff --git a/lib/mpl_toolkits/axisartist/axes_rgb.py b/lib/mpl_toolkits/axisartist/axes_rgb.py deleted file mode 100644 index 2195747469a1..000000000000 --- a/lib/mpl_toolkits/axisartist/axes_rgb.py +++ /dev/null @@ -1,18 +0,0 @@ -from matplotlib import _api -from mpl_toolkits.axes_grid1.axes_rgb import ( # noqa - make_rgb_axes, RGBAxes as _RGBAxes) -from .axislines import Axes - - -_api.warn_deprecated( - "3.8", name=__name__, obj_type="module", alternative="axes_grid1.axes_rgb") - - -@_api.deprecated("3.8", alternative=( - "axes_grid1.axes_rgb.RGBAxes(..., axes_class=axislines.Axes")) -class RGBAxes(_RGBAxes): - """ - Subclass of `~.axes_grid1.axes_rgb.RGBAxes` with - ``_defaultAxesClass`` = `.axislines.Axes`. - """ - _defaultAxesClass = Axes diff --git a/lib/mpl_toolkits/axisartist/axis_artist.py b/lib/mpl_toolkits/axisartist/axis_artist.py index d58313bd99ef..b416d56abe6b 100644 --- a/lib/mpl_toolkits/axisartist/axis_artist.py +++ b/lib/mpl_toolkits/axisartist/axis_artist.py @@ -312,13 +312,13 @@ def get_pad(self): def get_ref_artist(self): # docstring inherited - return self._axis.get_label() + return self._axis.label def get_text(self): # docstring inherited t = super().get_text() if t == "__from_axes__": - return self._axis.get_label().get_text() + return self._axis.label.get_text() return self._text _default_alignments = dict(left=("bottom", "center"), diff --git a/lib/mpl_toolkits/axisartist/axislines.py b/lib/mpl_toolkits/axisartist/axislines.py index 1d695c129ae2..8d06cb236269 100644 --- a/lib/mpl_toolkits/axisartist/axislines.py +++ b/lib/mpl_toolkits/axisartist/axislines.py @@ -370,10 +370,6 @@ def get_gridlines(self, which="major", axis="both"): class Axes(maxes.Axes): - @_api.deprecated("3.8", alternative="ax.axis") - def __call__(self, *args, **kwargs): - return maxes.Axes.axis(self.axes, *args, **kwargs) - def __init__(self, *args, grid_helper=None, **kwargs): self._axisline_on = True self._grid_helper = grid_helper if grid_helper else GridHelperRectlinear(self) diff --git a/lib/mpl_toolkits/axisartist/floating_axes.py b/lib/mpl_toolkits/axisartist/floating_axes.py index ecdcca5122bf..74e4c941879b 100644 --- a/lib/mpl_toolkits/axisartist/floating_axes.py +++ b/lib/mpl_toolkits/axisartist/floating_axes.py @@ -147,17 +147,6 @@ def __init__(self, aux_trans, extremes, tick_formatter1=tick_formatter1, tick_formatter2=tick_formatter2) - @_api.deprecated("3.8") - def get_data_boundary(self, side): - """ - Return v=0, nth=1. - """ - lon1, lon2, lat1, lat2 = self.grid_finder.extreme_finder(*[None] * 5) - return dict(left=(lon1, 0), - right=(lon2, 0), - bottom=(lat1, 1), - top=(lat2, 1))[side] - def new_fixed_axis( self, loc, nth_coord=None, axis_direction=None, offset=None, axes=None): if axes is None: diff --git a/lib/mpl_toolkits/axisartist/meson.build b/lib/mpl_toolkits/axisartist/meson.build index 8d9314e42576..6d95cf0dfdcd 100644 --- a/lib/mpl_toolkits/axisartist/meson.build +++ b/lib/mpl_toolkits/axisartist/meson.build @@ -2,8 +2,6 @@ python_sources = [ '__init__.py', 'angle_helper.py', 'axes_divider.py', - 'axes_grid.py', - 'axes_rgb.py', 'axis_artist.py', 'axislines.py', 'axisline_style.py', diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index 38ebe88dc80e..74106cfdf91b 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -73,6 +73,34 @@ def get_dir_vector(zdir): raise ValueError("'x', 'y', 'z', None or vector of length 3 expected") +def _viewlim_mask(xs, ys, zs, axes): + """ + Return original points with points outside the axes view limits masked. + + Parameters + ---------- + xs, ys, zs : array-like + The points to mask. + axes : Axes3D + The axes to use for the view limits. + + Returns + ------- + xs_masked, ys_masked, zs_masked : np.ma.array + The masked points. + """ + mask = np.logical_or.reduce((xs < axes.xy_viewLim.xmin, + xs > axes.xy_viewLim.xmax, + ys < axes.xy_viewLim.ymin, + ys > axes.xy_viewLim.ymax, + zs < axes.zz_viewLim.xmin, + zs > axes.zz_viewLim.xmax)) + xs_masked = np.ma.array(xs, mask=mask) + ys_masked = np.ma.array(ys, mask=mask) + zs_masked = np.ma.array(zs, mask=mask) + return xs_masked, ys_masked, zs_masked + + class Text3D(mtext.Text): """ Text object with 3D position and direction. @@ -86,6 +114,8 @@ class Text3D(mtext.Text): zdir : {'x', 'y', 'z', None, 3-tuple} The direction of the text. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide text outside the axes view limits. Other Parameters ---------------- @@ -93,9 +123,10 @@ class Text3D(mtext.Text): All other parameters are passed on to `~matplotlib.text.Text`. """ - def __init__(self, x=0, y=0, z=0, text='', zdir='z', **kwargs): + def __init__(self, x=0, y=0, z=0, text='', zdir='z', axlim_clip=False, + **kwargs): mtext.Text.__init__(self, x, y, text, **kwargs) - self.set_3d_properties(z, zdir) + self.set_3d_properties(z, zdir, axlim_clip) def get_position_3d(self): """Return the (x, y, z) position of the text.""" @@ -129,7 +160,7 @@ def set_z(self, z): self._z = z self.stale = True - def set_3d_properties(self, z=0, zdir='z'): + def set_3d_properties(self, z=0, zdir='z', axlim_clip=False): """ Set the *z* position and direction of the text. @@ -140,14 +171,23 @@ def set_3d_properties(self, z=0, zdir='z'): zdir : {'x', 'y', 'z', 3-tuple} The direction of the text. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide text outside the axes view limits. """ self._z = z self._dir_vec = get_dir_vector(zdir) + self._axlim_clip = axlim_clip self.stale = True @artist.allow_rasterization def draw(self, renderer): - position3d = np.array((self._x, self._y, self._z)) + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(self._x, self._y, self._z, self.axes) + position3d = np.ma.row_stack((xs, ys, zs)).ravel().filled(np.nan) + else: + xs, ys, zs = self._x, self._y, self._z + position3d = np.asanyarray([xs, ys, zs]) + proj = proj3d._proj_trans_points( [position3d, position3d + self._dir_vec], self.axes.M) dx = proj[0][1] - proj[0][0] @@ -164,7 +204,7 @@ def get_tightbbox(self, renderer=None): return None -def text_2d_to_3d(obj, z=0, zdir='z'): +def text_2d_to_3d(obj, z=0, zdir='z', axlim_clip=False): """ Convert a `.Text` to a `.Text3D` object. @@ -175,9 +215,11 @@ def text_2d_to_3d(obj, z=0, zdir='z'): zdir : {'x', 'y', 'z', 3-tuple} The direction of the text. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide text outside the axes view limits. """ obj.__class__ = Text3D - obj.set_3d_properties(z, zdir) + obj.set_3d_properties(z, zdir, axlim_clip) class Line3D(lines.Line2D): @@ -191,7 +233,7 @@ class Line3D(lines.Line2D): `~.Line2D.set_data`, `~.Line2D.set_xdata`, and `~.Line2D.set_ydata`. """ - def __init__(self, xs, ys, zs, *args, **kwargs): + def __init__(self, xs, ys, zs, *args, axlim_clip=False, **kwargs): """ Parameters @@ -207,8 +249,9 @@ def __init__(self, xs, ys, zs, *args, **kwargs): """ super().__init__([], [], *args, **kwargs) self.set_data_3d(xs, ys, zs) + self._axlim_clip = axlim_clip - def set_3d_properties(self, zs=0, zdir='z'): + def set_3d_properties(self, zs=0, zdir='z', axlim_clip=False): """ Set the *z* position and direction of the line. @@ -220,12 +263,15 @@ def set_3d_properties(self, zs=0, zdir='z'): zdir : {'x', 'y', 'z'} Plane to plot line orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide lines with an endpoint outside the axes view limits. """ xs = self.get_xdata() ys = self.get_ydata() zs = cbook._to_unmasked_float_array(zs).ravel() zs = np.broadcast_to(zs, len(xs)) self._verts3d = juggle_axes(xs, ys, zs, zdir) + self._axlim_clip = axlim_clip self.stale = True def set_data_3d(self, *args): @@ -266,7 +312,10 @@ def get_data_3d(self): @artist.allow_rasterization def draw(self, renderer): - xs3d, ys3d, zs3d = self._verts3d + if self._axlim_clip: + xs3d, ys3d, zs3d = _viewlim_mask(*self._verts3d, self.axes) + else: + xs3d, ys3d, zs3d = self._verts3d xs, ys, zs, tis = proj3d._proj_transform_clip(xs3d, ys3d, zs3d, self.axes.M, self.axes._focal_length) @@ -275,7 +324,7 @@ def draw(self, renderer): self.stale = False -def line_2d_to_3d(line, zs=0, zdir='z'): +def line_2d_to_3d(line, zs=0, zdir='z', axlim_clip=False): """ Convert a `.Line2D` to a `.Line3D` object. @@ -286,10 +335,12 @@ def line_2d_to_3d(line, zs=0, zdir='z'): zdir : {'x', 'y', 'z'} Plane to plot line orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide lines with an endpoint outside the axes view limits. """ line.__class__ = Line3D - line.set_3d_properties(zs, zdir) + line.set_3d_properties(zs, zdir, axlim_clip) def _path_to_3d_segment(path, zs=0, zdir='z'): @@ -351,15 +402,18 @@ class Collection3D(Collection): def do_3d_projection(self): """Project the points according to renderer matrix.""" - xyzs_list = [proj3d.proj_transform(*vs.T, self.axes.M) - for vs, _ in self._3dverts_codes] - self._paths = [mpath.Path(np.column_stack([xs, ys]), cs) + vs_list = [vs for vs, _ in self._3dverts_codes] + if self._axlim_clip: + vs_list = [np.ma.row_stack(_viewlim_mask(*vs.T, self.axes)).T + for vs in vs_list] + xyzs_list = [proj3d.proj_transform(*vs.T, self.axes.M) for vs in vs_list] + self._paths = [mpath.Path(np.ma.column_stack([xs, ys]), cs) for (xs, ys, _), (_, cs) in zip(xyzs_list, self._3dverts_codes)] zs = np.concatenate([zs for _, _, zs in xyzs_list]) return zs.min() if len(zs) else 1e9 -def collection_2d_to_3d(col, zs=0, zdir='z'): +def collection_2d_to_3d(col, zs=0, zdir='z', axlim_clip=False): """Convert a `.Collection` to a `.Collection3D` object.""" zs = np.broadcast_to(zs, len(col.get_paths())) col._3dverts_codes = [ @@ -369,12 +423,16 @@ def collection_2d_to_3d(col, zs=0, zdir='z'): p.codes) for p, z in zip(col.get_paths(), zs)] col.__class__ = cbook._make_class_factory(Collection3D, "{}3D")(type(col)) + col._axlim_clip = axlim_clip class Line3DCollection(LineCollection): """ A collection of 3D lines. """ + def __init__(self, lines, axlim_clip=False, **kwargs): + super().__init__(lines, **kwargs) + self._axlim_clip = axlim_clip def set_sort_zpos(self, val): """Set the position to use for z-sorting.""" @@ -392,9 +450,16 @@ def do_3d_projection(self): """ Project the points according to renderer matrix. """ + segments = self._segments3d + if self._axlim_clip: + all_points = np.ma.vstack(segments) + masked_points = np.ma.column_stack([*_viewlim_mask(*all_points.T, + self.axes)]) + segment_lengths = [segment.shape[0] for segment in segments] + segments = np.split(masked_points, np.cumsum(segment_lengths[:-1])) xyslist = [proj3d._proj_trans_points(points, self.axes.M) - for points in self._segments3d] - segments_2d = [np.column_stack([xs, ys]) for xs, ys, zs in xyslist] + for points in segments] + segments_2d = [np.ma.column_stack([xs, ys]) for xs, ys, zs in xyslist] LineCollection.set_segments(self, segments_2d) # FIXME @@ -404,11 +469,12 @@ def do_3d_projection(self): return minz -def line_collection_2d_to_3d(col, zs=0, zdir='z'): +def line_collection_2d_to_3d(col, zs=0, zdir='z', axlim_clip=False): """Convert a `.LineCollection` to a `.Line3DCollection` object.""" segments3d = _paths_to_3d_segments(col.get_paths(), zs, zdir) col.__class__ = Line3DCollection col.set_segments(segments3d) + col._axlim_clip = axlim_clip class Patch3D(Patch): @@ -416,7 +482,7 @@ class Patch3D(Patch): 3D patch object. """ - def __init__(self, *args, zs=(), zdir='z', **kwargs): + def __init__(self, *args, zs=(), zdir='z', axlim_clip=False, **kwargs): """ Parameters ---------- @@ -427,11 +493,13 @@ def __init__(self, *args, zs=(), zdir='z', **kwargs): zdir : {'x', 'y', 'z'} Plane to plot patch orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. """ super().__init__(*args, **kwargs) - self.set_3d_properties(zs, zdir) + self.set_3d_properties(zs, zdir, axlim_clip) - def set_3d_properties(self, verts, zs=0, zdir='z'): + def set_3d_properties(self, verts, zs=0, zdir='z', axlim_clip=False): """ Set the *z* position and direction of the patch. @@ -444,10 +512,13 @@ def set_3d_properties(self, verts, zs=0, zdir='z'): zdir : {'x', 'y', 'z'} Plane to plot patch orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. """ zs = np.broadcast_to(zs, len(verts)) self._segment3d = [juggle_axes(x, y, z, zdir) for ((x, y), z) in zip(verts, zs)] + self._axlim_clip = axlim_clip def get_path(self): # docstring inherited @@ -459,11 +530,14 @@ def get_path(self): def do_3d_projection(self): s = self._segment3d - xs, ys, zs = zip(*s) + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(*zip(*s), self.axes) + else: + xs, ys, zs = zip(*s) vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, self.axes.M, self.axes._focal_length) - self._path2d = mpath.Path(np.column_stack([vxs, vys])) + self._path2d = mpath.Path(np.ma.column_stack([vxs, vys])) return min(vzs) @@ -472,7 +546,7 @@ class PathPatch3D(Patch3D): 3D PathPatch object. """ - def __init__(self, path, *, zs=(), zdir='z', **kwargs): + def __init__(self, path, *, zs=(), zdir='z', axlim_clip=False, **kwargs): """ Parameters ---------- @@ -483,12 +557,14 @@ def __init__(self, path, *, zs=(), zdir='z', **kwargs): zdir : {'x', 'y', 'z', 3-tuple} Plane to plot path patch orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide path patches with a point outside the axes view limits. """ # Not super().__init__! Patch.__init__(self, **kwargs) - self.set_3d_properties(path, zs, zdir) + self.set_3d_properties(path, zs, zdir, axlim_clip) - def set_3d_properties(self, path, zs=0, zdir='z'): + def set_3d_properties(self, path, zs=0, zdir='z', axlim_clip=False): """ Set the *z* position and direction of the path patch. @@ -501,17 +577,23 @@ def set_3d_properties(self, path, zs=0, zdir='z'): zdir : {'x', 'y', 'z', 3-tuple} Plane to plot path patch orthogonal to. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide path patches with a point outside the axes view limits. """ - Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir) + Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir, + axlim_clip=axlim_clip) self._code3d = path.codes def do_3d_projection(self): s = self._segment3d - xs, ys, zs = zip(*s) + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(*zip(*s), self.axes) + else: + xs, ys, zs = zip(*s) vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, self.axes.M, self.axes._focal_length) - self._path2d = mpath.Path(np.column_stack([vxs, vys]), self._code3d) + self._path2d = mpath.Path(np.ma.column_stack([vxs, vys]), self._code3d) return min(vzs) @@ -523,11 +605,11 @@ def _get_patch_verts(patch): return polygons[0] if len(polygons) else np.array([]) -def patch_2d_to_3d(patch, z=0, zdir='z'): +def patch_2d_to_3d(patch, z=0, zdir='z', axlim_clip=False): """Convert a `.Patch` to a `.Patch3D` object.""" verts = _get_patch_verts(patch) patch.__class__ = Patch3D - patch.set_3d_properties(verts, z, zdir) + patch.set_3d_properties(verts, z, zdir, axlim_clip) def pathpatch_2d_to_3d(pathpatch, z=0, zdir='z'): @@ -545,7 +627,8 @@ class Patch3DCollection(PatchCollection): A collection of 3D patches. """ - def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): + def __init__(self, *args, + zs=0, zdir='z', depthshade=True, axlim_clip=False, **kwargs): """ Create a collection of flat 3D patches with its normal vector pointed in *zdir* direction, and located at *zs* on the *zdir* @@ -562,7 +645,7 @@ def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): """ self._depthshade = depthshade super().__init__(*args, **kwargs) - self.set_3d_properties(zs, zdir) + self.set_3d_properties(zs, zdir, axlim_clip) def get_depthshade(self): return self._depthshade @@ -585,7 +668,7 @@ def set_sort_zpos(self, val): self._sort_zpos = val self.stale = True - def set_3d_properties(self, zs, zdir): + def set_3d_properties(self, zs, zdir, axlim_clip=False): """ Set the *z* positions and direction of the patches. @@ -598,6 +681,8 @@ def set_3d_properties(self, zs, zdir): Plane to plot patches orthogonal to. All patches must have the same direction. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. """ # Force the collection to initialize the face and edgecolors # just in case it is a scalarmappable with a colormap. @@ -611,15 +696,19 @@ def set_3d_properties(self, zs, zdir): self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir) self._z_markers_idx = slice(-1) self._vzs = None + self._axlim_clip = axlim_clip self.stale = True def do_3d_projection(self): - xs, ys, zs = self._offsets3d + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(*self._offsets3d, self.axes) + else: + xs, ys, zs = self._offsets3d vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, self.axes.M, self.axes._focal_length) self._vzs = vzs - super().set_offsets(np.column_stack([vxs, vys])) + super().set_offsets(np.ma.column_stack([vxs, vys])) if vzs.size > 0: return min(vzs) @@ -653,7 +742,8 @@ class Path3DCollection(PathCollection): A collection of 3D paths. """ - def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): + def __init__(self, *args, + zs=0, zdir='z', depthshade=True, axlim_clip=False, **kwargs): """ Create a collection of flat 3D paths with its normal vector pointed in *zdir* direction, and located at *zs* on the *zdir* @@ -671,7 +761,7 @@ def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): self._depthshade = depthshade self._in_draw = False super().__init__(*args, **kwargs) - self.set_3d_properties(zs, zdir) + self.set_3d_properties(zs, zdir, axlim_clip) self._offset_zordered = None def draw(self, renderer): @@ -684,7 +774,7 @@ def set_sort_zpos(self, val): self._sort_zpos = val self.stale = True - def set_3d_properties(self, zs, zdir): + def set_3d_properties(self, zs, zdir, axlim_clip=False): """ Set the *z* positions and direction of the paths. @@ -697,6 +787,8 @@ def set_3d_properties(self, zs, zdir): Plane to plot paths orthogonal to. All paths must have the same direction. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide paths with a vertex outside the axes view limits. """ # Force the collection to initialize the face and edgecolors # just in case it is a scalarmappable with a colormap. @@ -707,6 +799,7 @@ def set_3d_properties(self, zs, zdir): else: xs = [] ys = [] + self._zdir = zdir self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir) # In the base draw methods we access the attributes directly which # means we cannot resolve the shuffling in the getter methods like @@ -727,6 +820,8 @@ def set_3d_properties(self, zs, zdir): # points and point properties according to the index array self._z_markers_idx = slice(-1) self._vzs = None + + self._axlim_clip = axlim_clip self.stale = True def set_sizes(self, sizes, dpi=72.0): @@ -756,14 +851,17 @@ def set_depthshade(self, depthshade): self.stale = True def do_3d_projection(self): - xs, ys, zs = self._offsets3d + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(*self._offsets3d, self.axes) + else: + xs, ys, zs = self._offsets3d vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs, self.axes.M, self.axes._focal_length) # Sort the points based on z coordinates # Performance optimization: Create a sorted index array and reorder # points and point properties according to the index array - z_markers_idx = self._z_markers_idx = np.argsort(vzs)[::-1] + z_markers_idx = self._z_markers_idx = np.ma.argsort(vzs)[::-1] self._vzs = vzs # we have to special case the sizes because of code in collections.py @@ -777,7 +875,7 @@ def do_3d_projection(self): if len(self._linewidths3d) > 1: self._linewidths = self._linewidths3d[z_markers_idx] - PathCollection.set_offsets(self, np.column_stack((vxs, vys))) + PathCollection.set_offsets(self, np.ma.column_stack((vxs, vys))) # Re-order items vzs = vzs[z_markers_idx] @@ -785,7 +883,7 @@ def do_3d_projection(self): vys = vys[z_markers_idx] # Store ordered offset for drawing purpose - self._offset_zordered = np.column_stack((vxs, vys)) + self._offset_zordered = np.ma.column_stack((vxs, vys)) return np.min(vzs) if vzs.size else np.nan @@ -825,7 +923,7 @@ def get_edgecolor(self): return self._maybe_depth_shade_and_sort_colors(super().get_edgecolor()) -def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True): +def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True, axlim_clip=False): """ Convert a `.PatchCollection` into a `.Patch3DCollection` object (or a `.PathCollection` into a `.Path3DCollection` object). @@ -843,7 +941,8 @@ def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True): See `.get_dir_vector` for a description of the values. depthshade : bool, default: True Whether to shade the patches to give a sense of depth. - + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. """ if isinstance(col, PathCollection): col.__class__ = Path3DCollection @@ -852,7 +951,7 @@ def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True): col.__class__ = Patch3DCollection col._depthshade = depthshade col._in_draw = False - col.set_3d_properties(zs, zdir) + col.set_3d_properties(zs, zdir, axlim_clip) class Poly3DCollection(PolyCollection): @@ -877,7 +976,7 @@ class Poly3DCollection(PolyCollection): """ def __init__(self, verts, *args, zsort='average', shade=False, - lightsource=None, **kwargs): + lightsource=None, axlim_clip=False, **kwargs): """ Parameters ---------- @@ -899,6 +998,9 @@ def __init__(self, verts, *args, zsort='average', shade=False, .. versionadded:: 3.7 + axlim_clip : bool, default: False + Whether to hide polygons with a vertex outside the view limits. + *args, **kwargs All other parameters are forwarded to `.PolyCollection`. @@ -933,6 +1035,7 @@ def __init__(self, verts, *args, zsort='average', shade=False, raise ValueError('verts must be a list of (N, 3) array-like') self.set_zsort(zsort) self._codes3d = None + self._axlim_clip = axlim_clip _zsort_functions = { 'average': np.average, @@ -997,7 +1100,7 @@ def set_verts_and_codes(self, verts, codes): # and set our own codes instead. self._codes3d = codes - def set_3d_properties(self): + def set_3d_properties(self, axlim_clip=False): # Force the collection to initialize the face and edgecolors # just in case it is a scalarmappable with a colormap. self.update_scalarmappable() @@ -1030,7 +1133,16 @@ def do_3d_projection(self): self._facecolor3d = self._facecolors if self._edge_is_mapped: self._edgecolor3d = self._edgecolors - txs, tys, tzs = proj3d._proj_transform_vec(self._vec, self.axes.M) + if self._axlim_clip: + xs, ys, zs = _viewlim_mask(*self._vec[0:3], self.axes) + if self._vec.shape[0] == 4: # Will be 3 (xyz) or 4 (xyzw) + w_masked = np.ma.masked_where(zs.mask, self._vec[3]) + vec = np.ma.array([xs, ys, zs, w_masked]) + else: + vec = np.ma.array([xs, ys, zs]) + else: + vec = self._vec + txs, tys, tzs = proj3d._proj_transform_vec(vec, self.axes.M) xyzlist = [(txs[sl], tys[sl], tzs[sl]) for sl in self._segslices] # This extra fuss is to re-order face / edge colors @@ -1047,7 +1159,7 @@ def do_3d_projection(self): if xyzlist: # sort by depth (furthest drawn first) z_segments_2d = sorted( - ((self._zsortfunc(zs), np.column_stack([xs, ys]), fc, ec, idx) + ((self._zsortfunc(zs.data), np.ma.column_stack([xs, ys]), fc, ec, idx) for idx, ((xs, ys, zs), fc, ec) in enumerate(zip(xyzlist, cface, cedge))), key=lambda x: x[0], reverse=True) @@ -1124,7 +1236,7 @@ def get_edgecolor(self): return np.asarray(self._edgecolors2d) -def poly_collection_2d_to_3d(col, zs=0, zdir='z'): +def poly_collection_2d_to_3d(col, zs=0, zdir='z', axlim_clip=False): """ Convert a `.PolyCollection` into a `.Poly3DCollection` object. @@ -1144,6 +1256,7 @@ def poly_collection_2d_to_3d(col, zs=0, zdir='z'): col.__class__ = Poly3DCollection col.set_verts_and_codes(segments_3d, codes) col.set_3d_properties() + col._axlim_clip = axlim_clip def juggle_axes(xs, ys, zs, zdir): diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 5d522cd0988a..0c68925f7200 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -1801,7 +1801,7 @@ def get_zlabel(self): """ Get the z-label text string. """ - label = self.zaxis.get_label() + label = self.zaxis.label return label.get_text() # Axes rectangle characteristics @@ -1888,7 +1888,7 @@ def get_zbound(self): else: return upper, lower - def text(self, x, y, z, s, zdir=None, **kwargs): + def text(self, x, y, z, s, zdir=None, *, axlim_clip=False, **kwargs): """ Add the text *s* to the 3D Axes at location *x*, *y*, *z* in data coordinates. @@ -1901,6 +1901,10 @@ def text(self, x, y, z, s, zdir=None, **kwargs): zdir : {'x', 'y', 'z', 3-tuple}, optional The direction to be used as the z-direction. Default: 'z'. See `.get_dir_vector` for a description of the values. + axlim_clip : bool, default: False + Whether to hide text that is outside the axes view limits. + + .. versionadded:: 3.10 **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.text`. @@ -1910,13 +1914,13 @@ def text(self, x, y, z, s, zdir=None, **kwargs): The created `.Text3D` instance. """ text = super().text(x, y, s, **kwargs) - art3d.text_2d_to_3d(text, z, zdir) + art3d.text_2d_to_3d(text, z, zdir, axlim_clip) return text text3D = text text2D = Axes.text - def plot(self, xs, ys, *args, zdir='z', **kwargs): + def plot(self, xs, ys, *args, zdir='z', axlim_clip=False, **kwargs): """ Plot 2D or 3D data. @@ -1931,6 +1935,10 @@ def plot(self, xs, ys, *args, zdir='z', **kwargs): each point. zdir : {'x', 'y', 'z'}, default: 'z' When plotting 2D data, the direction to use as z. + axlim_clip : bool, default: False + Whether to hide data that is outside the axes view limits. + + .. versionadded:: 3.10 **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.plot`. """ @@ -1950,7 +1958,7 @@ def plot(self, xs, ys, *args, zdir='z', **kwargs): lines = super().plot(xs, ys, *args, **kwargs) for line in lines: - art3d.line_2d_to_3d(line, zs=zs, zdir=zdir) + art3d.line_2d_to_3d(line, zs=zs, zdir=zdir, axlim_clip=axlim_clip) xs, ys, zs = art3d.juggle_axes(xs, ys, zs, zdir) self.auto_scale_xyz(xs, ys, zs, had_data) @@ -1960,7 +1968,7 @@ def plot(self, xs, ys, *args, zdir='z', **kwargs): def fill_between(self, x1, y1, z1, x2, y2, z2, *, where=None, mode='auto', facecolors=None, shade=None, - **kwargs): + axlim_clip=False, **kwargs): """ Fill the area between two 3D curves. @@ -2006,6 +2014,11 @@ def fill_between(self, x1, y1, z1, x2, y2, z2, *, Whether to shade the facecolors. If *None*, then defaults to *True* for 'quad' mode and *False* for 'polygon' mode. + axlim_clip : bool, default: False + Whether to hide data that is outside the axes view limits. + + .. versionadded:: 3.10 + **kwargs All other keyword arguments are passed on to `.Poly3DCollection`. @@ -2076,14 +2089,14 @@ def fill_between(self, x1, y1, z1, x2, y2, z2, *, polys.append(poly) polyc = art3d.Poly3DCollection(polys, facecolors=facecolors, shade=shade, - **kwargs) + axlim_clip=axlim_clip, **kwargs) self.add_collection(polyc) self.auto_scale_xyz([x1, x2], [y1, y2], [z1, z2], had_data) return polyc def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, - vmax=None, lightsource=None, **kwargs): + vmax=None, lightsource=None, axlim_clip=False, **kwargs): """ Create a surface plot. @@ -2148,6 +2161,11 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, lightsource : `~matplotlib.colors.LightSource`, optional The lightsource to use when *shade* is True. + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. + + .. versionadded:: 3.10 + **kwargs Other keyword arguments are forwarded to `.Poly3DCollection`. """ @@ -2248,9 +2266,9 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, if fcolors is not None: polyc = art3d.Poly3DCollection( polys, edgecolors=colset, facecolors=colset, shade=shade, - lightsource=lightsource, **kwargs) + lightsource=lightsource, axlim_clip=axlim_clip, **kwargs) elif cmap: - polyc = art3d.Poly3DCollection(polys, **kwargs) + polyc = art3d.Poly3DCollection(polys, axlim_clip=axlim_clip, **kwargs) # can't always vectorize, because polys might be jagged if isinstance(polys, np.ndarray): avg_z = polys[..., 2].mean(axis=-1) @@ -2268,15 +2286,15 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, color = np.array(mcolors.to_rgba(color)) polyc = art3d.Poly3DCollection( - polys, facecolors=color, shade=shade, - lightsource=lightsource, **kwargs) + polys, facecolors=color, shade=shade, lightsource=lightsource, + axlim_clip=axlim_clip, **kwargs) self.add_collection(polyc) self.auto_scale_xyz(X, Y, Z, had_data) return polyc - def plot_wireframe(self, X, Y, Z, **kwargs): + def plot_wireframe(self, X, Y, Z, *, axlim_clip=False, **kwargs): """ Plot a 3D wireframe. @@ -2292,6 +2310,12 @@ def plot_wireframe(self, X, Y, Z, **kwargs): X, Y, Z : 2D arrays Data values. + axlim_clip : bool, default: False + Whether to hide lines and patches with vertices outside the axes + view limits. + + .. versionadded:: 3.10 + rcount, ccount : int Maximum number of samples used in each direction. If the input data is larger, it will be downsampled (by slicing) to these @@ -2388,14 +2412,14 @@ def plot_wireframe(self, X, Y, Z, **kwargs): + [list(zip(xl, yl, zl)) for xl, yl, zl in zip(txlines, tylines, tzlines)]) - linec = art3d.Line3DCollection(lines, **kwargs) + linec = art3d.Line3DCollection(lines, axlim_clip=axlim_clip, **kwargs) self.add_collection(linec) self.auto_scale_xyz(X, Y, Z, had_data) return linec def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, - lightsource=None, **kwargs): + lightsource=None, axlim_clip=False, **kwargs): """ Plot a triangulated surface. @@ -2437,6 +2461,10 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, *cmap* is specified. lightsource : `~matplotlib.colors.LightSource`, optional The lightsource to use when *shade* is True. + axlim_clip : bool, default: False + Whether to hide patches with a vertex outside the axes view limits. + + .. versionadded:: 3.10 **kwargs All other keyword arguments are passed on to :class:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection` @@ -2473,7 +2501,8 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, verts = np.stack((xt, yt, zt), axis=-1) if cmap: - polyc = art3d.Poly3DCollection(verts, *args, **kwargs) + polyc = art3d.Poly3DCollection(verts, *args, + axlim_clip=axlim_clip, **kwargs) # average over the three points of each triangle avg_z = verts[:, :, 2].mean(axis=1) polyc.set_array(avg_z) @@ -2484,7 +2513,7 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, else: polyc = art3d.Poly3DCollection( verts, *args, shade=shade, lightsource=lightsource, - facecolors=color, **kwargs) + facecolors=color, axlim_clip=axlim_clip, **kwargs) self.add_collection(polyc) self.auto_scale_xyz(tri.x, tri.y, z, had_data) @@ -2520,18 +2549,21 @@ def _3d_extend_contour(self, cset, stride=5): cset.remove() def add_contour_set( - self, cset, extend3d=False, stride=5, zdir='z', offset=None): + self, cset, extend3d=False, stride=5, zdir='z', offset=None, + axlim_clip=False): zdir = '-' + zdir if extend3d: self._3d_extend_contour(cset, stride) else: art3d.collection_2d_to_3d( - cset, zs=offset if offset is not None else cset.levels, zdir=zdir) + cset, zs=offset if offset is not None else cset.levels, zdir=zdir, + axlim_clip=axlim_clip) - def add_contourf_set(self, cset, zdir='z', offset=None): - self._add_contourf_set(cset, zdir=zdir, offset=offset) + def add_contourf_set(self, cset, zdir='z', offset=None, *, axlim_clip=False): + self._add_contourf_set(cset, zdir=zdir, offset=offset, + axlim_clip=axlim_clip) - def _add_contourf_set(self, cset, zdir='z', offset=None): + def _add_contourf_set(self, cset, zdir='z', offset=None, axlim_clip=False): """ Returns ------- @@ -2550,12 +2582,14 @@ def _add_contourf_set(self, cset, zdir='z', offset=None): midpoints = np.append(midpoints, max_level) art3d.collection_2d_to_3d( - cset, zs=offset if offset is not None else midpoints, zdir=zdir) + cset, zs=offset if offset is not None else midpoints, zdir=zdir, + axlim_clip=axlim_clip) return midpoints @_preprocess_data() def contour(self, X, Y, Z, *args, - extend3d=False, stride=5, zdir='z', offset=None, **kwargs): + extend3d=False, stride=5, zdir='z', offset=None, axlim_clip=False, + **kwargs): """ Create a 3D contour plot. @@ -2572,6 +2606,10 @@ def contour(self, X, Y, Z, *args, offset : float, optional If specified, plot a projection of the contour lines at this position in a plane normal to *zdir*. + axlim_clip : bool, default: False + Whether to hide lines with a vertex outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER @@ -2586,7 +2624,7 @@ def contour(self, X, Y, Z, *args, jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) cset = super().contour(jX, jY, jZ, *args, **kwargs) - self.add_contour_set(cset, extend3d, stride, zdir, offset) + self.add_contour_set(cset, extend3d, stride, zdir, offset, axlim_clip) self.auto_scale_xyz(X, Y, Z, had_data) return cset @@ -2595,7 +2633,8 @@ def contour(self, X, Y, Z, *args, @_preprocess_data() def tricontour(self, *args, - extend3d=False, stride=5, zdir='z', offset=None, **kwargs): + extend3d=False, stride=5, zdir='z', offset=None, axlim_clip=False, + **kwargs): """ Create a 3D contour plot. @@ -2616,6 +2655,10 @@ def tricontour(self, *args, offset : float, optional If specified, plot a projection of the contour lines at this position in a plane normal to *zdir*. + axlim_clip : bool, default: False + Whether to hide lines with a vertex outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER *args, **kwargs @@ -2641,7 +2684,7 @@ def tricontour(self, *args, tri = Triangulation(jX, jY, tri.triangles, tri.mask) cset = super().tricontour(tri, jZ, *args, **kwargs) - self.add_contour_set(cset, extend3d, stride, zdir, offset) + self.add_contour_set(cset, extend3d, stride, zdir, offset, axlim_clip) self.auto_scale_xyz(X, Y, Z, had_data) return cset @@ -2657,7 +2700,8 @@ def _auto_scale_contourf(self, X, Y, Z, zdir, levels, had_data): self.auto_scale_xyz(*limits, had_data) @_preprocess_data() - def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs): + def contourf(self, X, Y, Z, *args, + zdir='z', offset=None, axlim_clip=False, **kwargs): """ Create a 3D filled contour plot. @@ -2670,6 +2714,10 @@ def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs): offset : float, optional If specified, plot a projection of the contour lines at this position in a plane normal to *zdir*. + axlim_clip : bool, default: False + Whether to hide lines with a vertex outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER *args, **kwargs @@ -2683,7 +2731,7 @@ def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs): jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) cset = super().contourf(jX, jY, jZ, *args, **kwargs) - levels = self._add_contourf_set(cset, zdir, offset) + levels = self._add_contourf_set(cset, zdir, offset, axlim_clip) self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data) return cset @@ -2691,7 +2739,7 @@ def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs): contourf3D = contourf @_preprocess_data() - def tricontourf(self, *args, zdir='z', offset=None, **kwargs): + def tricontourf(self, *args, zdir='z', offset=None, axlim_clip=False, **kwargs): """ Create a 3D filled contour plot. @@ -2708,6 +2756,10 @@ def tricontourf(self, *args, zdir='z', offset=None, **kwargs): offset : float, optional If specified, plot a projection of the contour lines at this position in a plane normal to zdir. + axlim_clip : bool, default: False + Whether to hide lines with a vertex outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER *args, **kwargs @@ -2734,12 +2786,13 @@ def tricontourf(self, *args, zdir='z', offset=None, **kwargs): tri = Triangulation(jX, jY, tri.triangles, tri.mask) cset = super().tricontourf(tri, jZ, *args, **kwargs) - levels = self._add_contourf_set(cset, zdir, offset) + levels = self._add_contourf_set(cset, zdir, offset, axlim_clip) self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data) return cset - def add_collection3d(self, col, zs=0, zdir='z', autolim=True): + def add_collection3d(self, col, zs=0, zdir='z', autolim=True, *, + axlim_clip=False): """ Add a 3D collection object to the plot. @@ -2763,6 +2816,10 @@ def add_collection3d(self, col, zs=0, zdir='z', autolim=True): The direction to use for the z-positions. autolim : bool, default: True Whether to update the data limits. + axlim_clip : bool, default: False + Whether to hide the scatter points outside the axes view limits. + + .. versionadded:: 3.10 """ had_data = self.has_data() @@ -2774,13 +2831,16 @@ def add_collection3d(self, col, zs=0, zdir='z', autolim=True): # object would also pass.) Maybe have a collection3d # abstract class to test for and exclude? if type(col) is mcoll.PolyCollection: - art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir) + art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir, + axlim_clip=axlim_clip) col.set_sort_zpos(zsortval) elif type(col) is mcoll.LineCollection: - art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir) + art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir, + axlim_clip=axlim_clip) col.set_sort_zpos(zsortval) elif type(col) is mcoll.PatchCollection: - art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir) + art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir, + axlim_clip=axlim_clip) col.set_sort_zpos(zsortval) if autolim: @@ -2801,8 +2861,9 @@ def add_collection3d(self, col, zs=0, zdir='z', autolim=True): @_preprocess_data(replace_names=["xs", "ys", "zs", "s", "edgecolors", "c", "facecolor", "facecolors", "color"]) - def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, - *args, **kwargs): + def scatter(self, xs, ys, + zs=0, zdir='z', s=20, c=None, depthshade=True, *args, + axlim_clip=False, **kwargs): """ Create a scatter plot. @@ -2838,6 +2899,10 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, Whether to shade the scatter markers to give the appearance of depth. Each call to ``scatter()`` will perform its depthshading independently. + axlim_clip : bool, default: False + Whether to hide the scatter points outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER **kwargs @@ -2866,7 +2931,8 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, patches = super().scatter(xs, ys, s=s, c=c, *args, **kwargs) art3d.patch_collection_2d_to_3d(patches, zs=zs, zdir=zdir, - depthshade=depthshade) + depthshade=depthshade, + axlim_clip=axlim_clip) if self._zmargin < 0.05 and xs.size > 0: self.set_zmargin(0.05) @@ -2878,7 +2944,8 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, scatter3D = scatter @_preprocess_data() - def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): + def bar(self, left, height, zs=0, zdir='z', *args, + axlim_clip=False, **kwargs): """ Add 2D bar(s). @@ -2893,6 +2960,10 @@ def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): used for all bars. zdir : {'x', 'y', 'z'}, default: 'z' When plotting 2D data, the direction to use as z ('x', 'y' or 'z'). + axlim_clip : bool, default: False + Whether to hide bars with points outside the axes view limits. + + .. versionadded:: 3.10 data : indexable object, optional DATA_PARAMETER_PLACEHOLDER **kwargs @@ -2915,7 +2986,7 @@ def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): vs = art3d._get_patch_verts(p) verts += vs.tolist() verts_zs += [z] * len(vs) - art3d.patch_2d_to_3d(p, z, zdir) + art3d.patch_2d_to_3d(p, z, zdir, axlim_clip) if 'alpha' in kwargs: p.set_alpha(kwargs['alpha']) @@ -2934,7 +3005,8 @@ def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): @_preprocess_data() def bar3d(self, x, y, z, dx, dy, dz, color=None, - zsort='average', shade=True, lightsource=None, *args, **kwargs): + zsort='average', shade=True, lightsource=None, *args, + axlim_clip=False, **kwargs): """ Generate a 3D barplot. @@ -2981,6 +3053,11 @@ def bar3d(self, x, y, z, dx, dy, dz, color=None, lightsource : `~matplotlib.colors.LightSource`, optional The lightsource to use when *shade* is True. + axlim_clip : bool, default: False + Whether to hide the bars with points outside the axes view limits. + + .. versionadded:: 3.10 + data : indexable object, optional DATA_PARAMETER_PLACEHOLDER @@ -3086,6 +3163,7 @@ def bar3d(self, x, y, z, dx, dy, dz, color=None, facecolors=facecolors, shade=shade, lightsource=lightsource, + axlim_clip=axlim_clip, *args, **kwargs) self.add_collection(col) @@ -3103,7 +3181,7 @@ def set_title(self, label, fontdict=None, loc='center', **kwargs): @_preprocess_data() def quiver(self, X, Y, Z, U, V, W, *, length=1, arrow_length_ratio=.3, pivot='tail', normalize=False, - **kwargs): + axlim_clip=False, **kwargs): """ Plot a 3D field of arrows. @@ -3135,6 +3213,11 @@ def quiver(self, X, Y, Z, U, V, W, *, Whether all arrows are normalized to have the same length, or keep the lengths defined by *u*, *v*, and *w*. + axlim_clip : bool, default: False + Whether to hide arrows with points outside the axes view limits. + + .. versionadded:: 3.10 + data : indexable object, optional DATA_PARAMETER_PLACEHOLDER @@ -3216,7 +3299,7 @@ def calc_arrows(UVW): else: lines = [] - linec = art3d.Line3DCollection(lines, **kwargs) + linec = art3d.Line3DCollection(lines, axlim_clip=axlim_clip, **kwargs) self.add_collection(linec) self.auto_scale_xyz(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], had_data) @@ -3226,7 +3309,7 @@ def calc_arrows(UVW): quiver3D = quiver def voxels(self, *args, facecolors=None, edgecolors=None, shade=True, - lightsource=None, **kwargs): + lightsource=None, axlim_clip=False, **kwargs): """ ax.voxels([x, y, z,] /, filled, facecolors=None, edgecolors=None, \ **kwargs) @@ -3273,6 +3356,11 @@ def voxels(self, *args, facecolors=None, edgecolors=None, shade=True, lightsource : `~matplotlib.colors.LightSource`, optional The lightsource to use when *shade* is True. + axlim_clip : bool, default: False + Whether to hide voxels with points outside the axes view limits. + + .. versionadded:: 3.10 + **kwargs Additional keyword arguments to pass onto `~mpl_toolkits.mplot3d.art3d.Poly3DCollection`. @@ -3428,7 +3516,8 @@ def permutation_matrices(n): poly = art3d.Poly3DCollection( faces, facecolors=facecolor, edgecolors=edgecolor, - shade=shade, lightsource=lightsource, **kwargs) + shade=shade, lightsource=lightsource, axlim_clip=axlim_clip, + **kwargs) self.add_collection3d(poly) polygons[coord] = poly @@ -3439,6 +3528,7 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', barsabove=False, errorevery=1, ecolor=None, elinewidth=None, capsize=None, capthick=None, xlolims=False, xuplims=False, ylolims=False, yuplims=False, zlolims=False, zuplims=False, + axlim_clip=False, **kwargs): """ Plot lines and/or markers with errorbars around them. @@ -3516,6 +3606,11 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', Used to avoid overlapping error bars when two series share x-axis values. + axlim_clip : bool, default: False + Whether to hide error bars that are outside the axes limits. + + .. versionadded:: 3.10 + Returns ------- errlines : list @@ -3571,7 +3666,7 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', # data processing. (data_line, base_style), = self._get_lines._plot_args( self, (x, y) if fmt == '' else (x, y, fmt), kwargs, return_kwargs=True) - art3d.line_2d_to_3d(data_line, zs=z) + art3d.line_2d_to_3d(data_line, zs=z, axlim_clip=axlim_clip) # Do this after creating `data_line` to avoid modifying `base_style`. if barsabove: @@ -3715,9 +3810,11 @@ def _extract_errs(err, data, lomask, himask): # these markers will rotate as the viewing angle changes cap_lo = art3d.Line3D(*lo_caps_xyz, ls='', marker=capmarker[i_zdir], + axlim_clip=axlim_clip, **eb_cap_style) cap_hi = art3d.Line3D(*hi_caps_xyz, ls='', marker=capmarker[i_zdir], + axlim_clip=axlim_clip, **eb_cap_style) self.add_line(cap_lo) self.add_line(cap_hi) @@ -3732,6 +3829,7 @@ def _extract_errs(err, data, lomask, himask): self.quiver(xl0, yl0, zl0, *-dir_vector, **eb_quiver_style) errline = art3d.Line3DCollection(np.array(coorderr).T, + axlim_clip=axlim_clip, **eb_lines_style) self.add_collection(errline) errlines.append(errline) @@ -3776,7 +3874,7 @@ def get_tightbbox(self, renderer=None, *, call_axes_locator=True, @_preprocess_data() def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', - bottom=0, label=None, orientation='z'): + bottom=0, label=None, orientation='z', axlim_clip=False): """ Create a 3D stem plot. @@ -3826,6 +3924,11 @@ def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', orientation : {'x', 'y', 'z'}, default: 'z' The direction along which stems are drawn. + axlim_clip : bool, default: False + Whether to hide stems that are outside the axes limits. + + .. versionadded:: 3.10 + data : indexable object, optional DATA_PARAMETER_PLACEHOLDER @@ -3877,7 +3980,8 @@ def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', baseline, = self.plot(basex, basey, basefmt, zs=bottom, zdir=orientation, label='_nolegend_') stemlines = art3d.Line3DCollection( - lines, linestyles=linestyle, colors=linecolor, label='_nolegend_') + lines, linestyles=linestyle, colors=linecolor, label='_nolegend_', + axlim_clip=axlim_clip) self.add_collection(stemlines) markerline, = self.plot(x, y, z, markerfmt, label='_nolegend_') diff --git a/lib/mpl_toolkits/mplot3d/axis3d.py b/lib/mpl_toolkits/mplot3d/axis3d.py index 0562b421e22c..4da5031b990c 100644 --- a/lib/mpl_toolkits/mplot3d/axis3d.py +++ b/lib/mpl_toolkits/mplot3d/axis3d.py @@ -195,11 +195,6 @@ def set_ticks_position(self, position): position : {'lower', 'upper', 'both', 'default', 'none'} The position of the bolded axis lines, ticks, and tick labels. """ - if position in ['top', 'bottom']: - _api.warn_deprecated('3.8', name=f'{position=}', - obj_type='argument value', - alternative="'upper' or 'lower'") - return _api.check_in_list(['lower', 'upper', 'both', 'default', 'none'], position=position) self._tick_position = position @@ -224,11 +219,6 @@ def set_label_position(self, position): position : {'lower', 'upper', 'both', 'default', 'none'} The position of the axis label. """ - if position in ['top', 'bottom']: - _api.warn_deprecated('3.8', name=f'{position=}', - obj_type='argument value', - alternative="'upper' or 'lower'") - return _api.check_in_list(['lower', 'upper', 'both', 'default', 'none'], position=position) self._label_position = position diff --git a/lib/mpl_toolkits/mplot3d/proj3d.py b/lib/mpl_toolkits/mplot3d/proj3d.py index efee2699767e..923bd32c9ce0 100644 --- a/lib/mpl_toolkits/mplot3d/proj3d.py +++ b/lib/mpl_toolkits/mplot3d/proj3d.py @@ -132,20 +132,33 @@ def _ortho_transformation(zfront, zback): def _proj_transform_vec(vec, M): - vecw = np.dot(M, vec) + vecw = np.dot(M, vec.data) w = vecw[3] txs, tys, tzs = vecw[0]/w, vecw[1]/w, vecw[2]/w + if np.ma.isMA(vec[0]): # we check each to protect for scalars + txs = np.ma.array(txs, mask=vec[0].mask) + if np.ma.isMA(vec[1]): + tys = np.ma.array(tys, mask=vec[1].mask) + if np.ma.isMA(vec[2]): + tzs = np.ma.array(tzs, mask=vec[2].mask) return txs, tys, tzs def _proj_transform_vec_clip(vec, M, focal_length): - vecw = np.dot(M, vec) + vecw = np.dot(M, vec.data) w = vecw[3] txs, tys, tzs = vecw[0] / w, vecw[1] / w, vecw[2] / w if np.isinf(focal_length): # don't clip orthographic projection tis = np.ones(txs.shape, dtype=bool) else: tis = (-1 <= txs) & (txs <= 1) & (-1 <= tys) & (tys <= 1) & (tzs <= 0) + if np.ma.isMA(vec[0]): + tis = tis & ~vec[0].mask + if np.ma.isMA(vec[1]): + tis = tis & ~vec[1].mask + if np.ma.isMA(vec[2]): + tis = tis & ~vec[2].mask + txs = np.ma.masked_array(txs, ~tis) tys = np.ma.masked_array(tys, ~tis) tzs = np.ma.masked_array(tzs, ~tis) @@ -167,7 +180,10 @@ def inv_transform(xs, ys, zs, invM): def _vec_pad_ones(xs, ys, zs): - return np.array([xs, ys, zs, np.ones_like(xs)]) + if np.ma.isMA(xs) or np.ma.isMA(ys) or np.ma.isMA(zs): + return np.ma.array([xs, ys, zs, np.ones_like(xs)]) + else: + return np.array([xs, ys, zs, np.ones_like(xs)]) def proj_transform(xs, ys, zs, M): @@ -198,5 +214,6 @@ def _proj_points(points, M): def _proj_trans_points(points, M): - xs, ys, zs = zip(*points) + points = np.asanyarray(points) + xs, ys, zs = points[:, 0], points[:, 1], points[:, 2] return proj_transform(xs, ys, zs, M) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 0afcae99c980..30581c146adb 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -1,6 +1,7 @@ import functools import itertools import platform +import sys import pytest @@ -115,7 +116,7 @@ def test_axes3d_repr(): @mpl3d_image_comparison(['axes3d_primary_views.png'], style='mpl20', - tol=0.05 if platform.machine() == "arm64" else 0) + tol=0.05 if sys.platform == "darwin" else 0) def test_axes3d_primary_views(): # (elev, azim, roll) views = [(90, -90, 0), # XY @@ -1378,6 +1379,45 @@ def test_axes3d_isometric(): ax.grid(True) +@check_figures_equal(extensions=["png"]) +def test_axlim_clip(fig_test, fig_ref): + # With axlim clipping + ax = fig_test.add_subplot(projection="3d") + x = np.linspace(0, 1, 11) + y = np.linspace(0, 1, 11) + X, Y = np.meshgrid(x, y) + Z = X + Y + ax.plot_surface(X, Y, Z, facecolor='C1', edgecolors=None, + rcount=50, ccount=50, axlim_clip=True) + # This ax.plot is to cover the extra surface edge which is not clipped out + ax.plot([0.5, 0.5], [0, 1], [0.5, 1.5], + color='k', linewidth=3, zorder=5, axlim_clip=True) + ax.scatter(X.ravel(), Y.ravel(), Z.ravel() + 1, axlim_clip=True) + ax.quiver(X.ravel(), Y.ravel(), Z.ravel() + 2, + 0*X.ravel(), 0*Y.ravel(), 0*Z.ravel() + 1, + arrow_length_ratio=0, axlim_clip=True) + ax.plot(X[0], Y[0], Z[0] + 3, color='C2', axlim_clip=True) + ax.text(1.1, 0.5, 4, 'test', axlim_clip=True) # won't be visible + ax.set(xlim=(0, 0.5), ylim=(0, 1), zlim=(0, 5)) + + # With manual clipping + ax = fig_ref.add_subplot(projection="3d") + idx = (X <= 0.5) + X = X[idx].reshape(11, 6) + Y = Y[idx].reshape(11, 6) + Z = Z[idx].reshape(11, 6) + ax.plot_surface(X, Y, Z, facecolor='C1', edgecolors=None, + rcount=50, ccount=50, axlim_clip=False) + ax.plot([0.5, 0.5], [0, 1], [0.5, 1.5], + color='k', linewidth=3, zorder=5, axlim_clip=False) + ax.scatter(X.ravel(), Y.ravel(), Z.ravel() + 1, axlim_clip=False) + ax.quiver(X.ravel(), Y.ravel(), Z.ravel() + 2, + 0*X.ravel(), 0*Y.ravel(), 0*Z.ravel() + 1, + arrow_length_ratio=0, axlim_clip=False) + ax.plot(X[0], Y[0], Z[0] + 3, color='C2', axlim_clip=False) + ax.set(xlim=(0, 0.5), ylim=(0, 1), zlim=(0, 5)) + + @pytest.mark.parametrize('value', [np.inf, np.nan]) @pytest.mark.parametrize(('setter', 'side'), [ ('set_xlim3d', 'left'), diff --git a/pyproject.toml b/pyproject.toml index b706d86cb7b2..cd0a5c039758 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ classifiers=[ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Topic :: Scientific/Engineering :: Visualization", ] @@ -46,7 +47,6 @@ requires-python = ">=3.10" # Should be a copy of the build dependencies below. dev = [ "meson-python>=0.13.1", - "numpy>=1.25", "pybind11>=2.6,!=2.13.3", "setuptools_scm>=7", # Not required by us but setuptools_scm without a version, cso _if_ @@ -73,18 +73,6 @@ requires = [ "meson-python>=0.13.1", "pybind11>=2.6,!=2.13.3", "setuptools_scm>=7", - - # Comments on numpy build requirement range: - # - # 1. >=2.0.x is the numpy requirement for wheel builds for distribution - # on PyPI - building against 2.x yields wheels that are also - # ABI-compatible with numpy 1.x at runtime. - # 2. Note that building against numpy 1.x works fine too - users and - # redistributors can do this by installing the numpy version they like - # and disabling build isolation. - # 3. The <2.3 upper bound is for matching the numpy deprecation policy, - # it should not be loosened. - "numpy>=2.0.0rc1,<2.3", ] [tool.meson-python.args] diff --git a/requirements/dev/build-requirements.txt b/requirements/dev/build-requirements.txt index 0861a11c9ee5..b5cb6acdb279 100644 --- a/requirements/dev/build-requirements.txt +++ b/requirements/dev/build-requirements.txt @@ -1,4 +1,3 @@ pybind11!=2.13.3 meson-python -numpy<2.1.0 setuptools-scm diff --git a/requirements/testing/mypy.txt b/requirements/testing/mypy.txt index 0b65050b52de..aa20581ee69b 100644 --- a/requirements/testing/mypy.txt +++ b/requirements/testing/mypy.txt @@ -18,7 +18,6 @@ contourpy>=1.0.1 cycler>=0.10 fonttools>=4.22.0 kiwisolver>=1.3.1 -numpy>=1.19 packaging>=20.0 pillow>=8 pyparsing>=2.3.1 diff --git a/src/_backend_agg.cpp b/src/_backend_agg.cpp index ce88f504dc1e..eed27323ba9e 100644 --- a/src/_backend_agg.cpp +++ b/src/_backend_agg.cpp @@ -29,6 +29,16 @@ RendererAgg::RendererAgg(unsigned int width, unsigned int height, double dpi) lastclippath(NULL), _fill_color(agg::rgba(1, 1, 1, 0)) { + if (dpi <= 0.0) { + throw std::range_error("dpi must be positive"); + } + + if (width >= 1 << 23 || height >= 1 << 23) { + throw std::range_error( + "Image size of " + std::to_string(width) + "x" + std::to_string(height) + + " pixels is too large. It must be less than 2^23 in each direction."); + } + unsigned stride(width * 4); pixBuffer = new agg::int8u[NUMBYTES]; diff --git a/src/_backend_agg.h b/src/_backend_agg.h index 470d459de341..b14eb3f9e565 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -6,8 +6,11 @@ #ifndef MPL_BACKEND_AGG_H #define MPL_BACKEND_AGG_H +#include + #include #include +#include #include "agg_alpha_mask_u8.h" #include "agg_conv_curve.h" @@ -40,6 +43,8 @@ #include "array.h" #include "agg_workaround.h" +namespace py = pybind11; + /**********************************************************************/ // a helper class to pass agg::buffer objects around. @@ -112,10 +117,10 @@ class RendererAgg typedef agg::renderer_scanline_bin_solid renderer_bin; typedef agg::rasterizer_scanline_aa rasterizer; - typedef agg::scanline_p8 scanline_p8; - typedef agg::scanline_bin scanline_bin; + typedef agg::scanline32_p8 scanline_p8; + typedef agg::scanline32_bin scanline_bin; typedef agg::amask_no_clip_gray8 alpha_mask_type; - typedef agg::scanline_u8_am scanline_am; + typedef agg::scanline32_u8_am scanline_am; typedef agg::renderer_base renderer_base_alpha_mask_type; typedef agg::renderer_scanline_aa_solid renderer_alpha_mask_type; @@ -728,7 +733,7 @@ inline void RendererAgg::draw_text_image(GCAgg &gc, ImageArray &image, int x, in rendererBase.reset_clipping(true); if (angle != 0.0) { agg::rendering_buffer srcbuf( - image.data(), (unsigned)image.shape(1), + image.mutable_data(0, 0), (unsigned)image.shape(1), (unsigned)image.shape(0), (unsigned)image.shape(1)); agg::pixfmt_gray8 pixf_img(srcbuf); @@ -828,8 +833,9 @@ inline void RendererAgg::draw_image(GCAgg &gc, bool has_clippath = render_clippath(gc.clippath.path, gc.clippath.trans, gc.snap_mode); agg::rendering_buffer buffer; - buffer.attach( - image.data(), (unsigned)image.shape(1), (unsigned)image.shape(0), -(int)image.shape(1) * 4); + buffer.attach(image.mutable_data(0, 0, 0), + (unsigned)image.shape(1), (unsigned)image.shape(0), + -(int)image.shape(1) * 4); pixfmt pixf(buffer); if (has_clippath) { @@ -913,6 +919,10 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, typedef PathSnapper snapped_t; typedef agg::conv_curve snapped_curve_t; typedef agg::conv_curve curve_t; + typedef Sketch sketch_clipped_t; + typedef Sketch sketch_curve_t; + typedef Sketch sketch_snapped_t; + typedef Sketch sketch_snapped_curve_t; size_t Npaths = path_generator.num_paths(); size_t Noffsets = safe_first_shape(offsets); @@ -988,31 +998,29 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, } } + gc.isaa = antialiaseds(i % Naa); + transformed_path_t tpath(path, trans); + nan_removed_t nan_removed(tpath, true, has_codes); + clipped_t clipped(nan_removed, do_clip, width, height); if (check_snap) { - gc.isaa = antialiaseds(i % Naa); - - transformed_path_t tpath(path, trans); - nan_removed_t nan_removed(tpath, true, has_codes); - clipped_t clipped(nan_removed, do_clip, width, height); snapped_t snapped( clipped, gc.snap_mode, path.total_vertices(), points_to_pixels(gc.linewidth)); if (has_codes) { snapped_curve_t curve(snapped); - _draw_path(curve, has_clippath, face, gc); + sketch_snapped_curve_t sketch(curve, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + _draw_path(sketch, has_clippath, face, gc); } else { - _draw_path(snapped, has_clippath, face, gc); + sketch_snapped_t sketch(snapped, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + _draw_path(sketch, has_clippath, face, gc); } } else { - gc.isaa = antialiaseds(i % Naa); - - transformed_path_t tpath(path, trans); - nan_removed_t nan_removed(tpath, true, has_codes); - clipped_t clipped(nan_removed, do_clip, width, height); if (has_codes) { curve_t curve(clipped); - _draw_path(curve, has_clippath, face, gc); + sketch_curve_t sketch(curve, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + _draw_path(sketch, has_clippath, face, gc); } else { - _draw_path(clipped, has_clippath, face, gc); + sketch_clipped_t sketch(clipped, gc.sketch.scale, gc.sketch.length, gc.sketch.randomness); + _draw_path(sketch, has_clippath, face, gc); } } } @@ -1226,14 +1234,27 @@ inline void RendererAgg::draw_gouraud_triangles(GCAgg &gc, ColorArray &colors, agg::trans_affine &trans) { + if (points.shape(0)) { + check_trailing_shape(points, "points", 3, 2); + } + if (colors.shape(0)) { + check_trailing_shape(colors, "colors", 3, 4); + } + if (points.shape(0) != colors.shape(0)) { + throw py::value_error( + "points and colors arrays must be the same length, got " + + std::to_string(points.shape(0)) + " points and " + + std::to_string(colors.shape(0)) + "colors"); + } + theRasterizer.reset_clipping(); rendererBase.reset_clipping(true); set_clipbox(gc.cliprect, theRasterizer); bool has_clippath = render_clippath(gc.clippath.path, gc.clippath.trans, gc.snap_mode); for (int i = 0; i < points.shape(0); ++i) { - typename PointArray::sub_t point = points.subarray(i); - typename ColorArray::sub_t color = colors.subarray(i); + auto point = std::bind(points, i, std::placeholders::_1, std::placeholders::_2); + auto color = std::bind(colors, i, std::placeholders::_1, std::placeholders::_2); _draw_gouraud_triangle(point, color, trans, has_clippath); } diff --git a/src/_backend_agg_basic_types.h b/src/_backend_agg_basic_types.h index 4fbf846d8cb4..e3e6be9a4532 100644 --- a/src/_backend_agg_basic_types.h +++ b/src/_backend_agg_basic_types.h @@ -4,6 +4,9 @@ /* Contains some simple types from the Agg backend that are also used by other modules */ +#include + +#include #include #include "agg_color_rgba.h" @@ -13,6 +16,8 @@ #include "py_adaptors.h" +namespace py = pybind11; + struct ClipPath { mpl::PathIterator path; @@ -121,4 +126,132 @@ class GCAgg GCAgg &operator=(const GCAgg &); }; +namespace PYBIND11_NAMESPACE { namespace detail { + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::line_cap_e, const_name("line_cap_e")); + + bool load(handle src, bool) { + const std::unordered_map enum_values = { + {"butt", agg::butt_cap}, + {"round", agg::round_cap}, + {"projecting", agg::square_cap}, + }; + value = enum_values.at(src.cast()); + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(agg::line_join_e, const_name("line_join_e")); + + bool load(handle src, bool) { + const std::unordered_map enum_values = { + {"miter", agg::miter_join_revert}, + {"round", agg::round_join}, + {"bevel", agg::bevel_join}, + }; + value = enum_values.at(src.cast()); + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(ClipPath, const_name("ClipPath")); + + bool load(handle src, bool) { + if (src.is_none()) { + return true; + } + + auto [path, trans] = + src.cast, agg::trans_affine>>(); + if (path) { + value.path = *path; + } + value.trans = trans; + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(Dashes, const_name("Dashes")); + + bool load(handle src, bool) { + auto [dash_offset, dashes_seq_or_none] = + src.cast>>(); + + if (!dashes_seq_or_none) { + return true; + } + + auto dashes_seq = *dashes_seq_or_none; + + auto nentries = dashes_seq.size(); + // If the dashpattern has odd length, iterate through it twice (in + // accordance with the pdf/ps/svg specs). + auto dash_pattern_length = (nentries % 2) ? 2 * nentries : nentries; + + for (py::size_t i = 0; i < dash_pattern_length; i += 2) { + auto length = dashes_seq[i % nentries].cast(); + auto skip = dashes_seq[(i + 1) % nentries].cast(); + + value.add_dash_pair(length, skip); + } + + value.set_dash_offset(dash_offset); + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(SketchParams, const_name("SketchParams")); + + bool load(handle src, bool) { + if (src.is_none()) { + value.scale = 0.0; + value.length = 0.0; + value.randomness = 0.0; + return true; + } + + auto params = src.cast>(); + std::tie(value.scale, value.length, value.randomness) = params; + + return true; + } + }; + + template <> struct type_caster { + public: + PYBIND11_TYPE_CASTER(GCAgg, const_name("GCAgg")); + + bool load(handle src, bool) { + value.linewidth = src.attr("_linewidth").cast(); + value.alpha = src.attr("_alpha").cast(); + value.forced_alpha = src.attr("_forced_alpha").cast(); + value.color = src.attr("_rgb").cast(); + value.isaa = src.attr("_antialiased").cast(); + value.cap = src.attr("_capstyle").cast(); + value.join = src.attr("_joinstyle").cast(); + value.dashes = src.attr("get_dashes")().cast(); + value.cliprect = src.attr("_cliprect").cast(); + value.clippath = src.attr("get_clip_path")().cast(); + value.snap_mode = src.attr("get_snap")().cast(); + value.hatchpath = src.attr("get_hatch_path")().cast(); + value.hatch_color = src.attr("get_hatch_color")().cast(); + value.hatch_linewidth = src.attr("get_hatch_linewidth")().cast(); + value.sketch = src.attr("get_sketch_params")().cast(); + + return true; + } + }; +}} // namespace PYBIND11_NAMESPACE::detail + #endif diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index eaf4bf6f5f9d..bfc8584d688d 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -1,566 +1,257 @@ +#include +#include +#include #include "mplutils.h" -#include "numpy_cpp.h" #include "py_converters.h" #include "_backend_agg.h" -typedef struct -{ - PyObject_HEAD - RendererAgg *x; - Py_ssize_t shape[3]; - Py_ssize_t strides[3]; - Py_ssize_t suboffsets[3]; -} PyRendererAgg; - -static PyTypeObject PyRendererAggType; - -typedef struct -{ - PyObject_HEAD - BufferRegion *x; - Py_ssize_t shape[3]; - Py_ssize_t strides[3]; - Py_ssize_t suboffsets[3]; -} PyBufferRegion; - -static PyTypeObject PyBufferRegionType; - +namespace py = pybind11; +using namespace pybind11::literals; /********************************************************************** * BufferRegion * */ -static PyObject *PyBufferRegion_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - PyBufferRegion *self; - self = (PyBufferRegion *)type->tp_alloc(type, 0); - self->x = NULL; - return (PyObject *)self; -} - -static void PyBufferRegion_dealloc(PyBufferRegion *self) -{ - delete self->x; - Py_TYPE(self)->tp_free((PyObject *)self); -} - /* TODO: This doesn't seem to be used internally. Remove? */ -static PyObject *PyBufferRegion_set_x(PyBufferRegion *self, PyObject *args) +static void +PyBufferRegion_set_x(BufferRegion *self, int x) { - int x; - if (!PyArg_ParseTuple(args, "i:set_x", &x)) { - return NULL; - } - self->x->get_rect().x1 = x; - - Py_RETURN_NONE; + self->get_rect().x1 = x; } -static PyObject *PyBufferRegion_set_y(PyBufferRegion *self, PyObject *args) +static void +PyBufferRegion_set_y(BufferRegion *self, int y) { - int y; - if (!PyArg_ParseTuple(args, "i:set_y", &y)) { - return NULL; - } - self->x->get_rect().y1 = y; - - Py_RETURN_NONE; + self->get_rect().y1 = y; } -static PyObject *PyBufferRegion_get_extents(PyBufferRegion *self, PyObject *args) +static py::object +PyBufferRegion_get_extents(BufferRegion *self) { - agg::rect_i rect = self->x->get_rect(); + agg::rect_i rect = self->get_rect(); - return Py_BuildValue("IIII", rect.x1, rect.y1, rect.x2, rect.y2); -} - -int PyBufferRegion_get_buffer(PyBufferRegion *self, Py_buffer *buf, int flags) -{ - Py_INCREF(self); - buf->obj = (PyObject *)self; - buf->buf = self->x->get_data(); - buf->len = (Py_ssize_t)self->x->get_width() * (Py_ssize_t)self->x->get_height() * 4; - buf->readonly = 0; - buf->format = (char *)"B"; - buf->ndim = 3; - self->shape[0] = self->x->get_height(); - self->shape[1] = self->x->get_width(); - self->shape[2] = 4; - buf->shape = self->shape; - self->strides[0] = self->x->get_width() * 4; - self->strides[1] = 4; - self->strides[2] = 1; - buf->strides = self->strides; - buf->suboffsets = NULL; - buf->itemsize = 1; - buf->internal = NULL; - - return 1; -} - -static PyTypeObject *PyBufferRegion_init_type() -{ - static PyMethodDef methods[] = { - { "set_x", (PyCFunction)PyBufferRegion_set_x, METH_VARARGS, NULL }, - { "set_y", (PyCFunction)PyBufferRegion_set_y, METH_VARARGS, NULL }, - { "get_extents", (PyCFunction)PyBufferRegion_get_extents, METH_NOARGS, NULL }, - { NULL } - }; - - static PyBufferProcs buffer_procs; - buffer_procs.bf_getbuffer = (getbufferproc)PyBufferRegion_get_buffer; - - PyBufferRegionType.tp_name = "matplotlib.backends._backend_agg.BufferRegion"; - PyBufferRegionType.tp_basicsize = sizeof(PyBufferRegion); - PyBufferRegionType.tp_dealloc = (destructor)PyBufferRegion_dealloc; - PyBufferRegionType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; - PyBufferRegionType.tp_methods = methods; - PyBufferRegionType.tp_new = PyBufferRegion_new; - PyBufferRegionType.tp_as_buffer = &buffer_procs; - - return &PyBufferRegionType; + return py::make_tuple(rect.x1, rect.y1, rect.x2, rect.y2); } /********************************************************************** * RendererAgg * */ -static PyObject *PyRendererAgg_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +static void +PyRendererAgg_draw_path(RendererAgg *self, + GCAgg &gc, + mpl::PathIterator path, + agg::trans_affine trans, + py::object rgbFace) { - PyRendererAgg *self; - self = (PyRendererAgg *)type->tp_alloc(type, 0); - self->x = NULL; - return (PyObject *)self; -} - -static int PyRendererAgg_init(PyRendererAgg *self, PyObject *args, PyObject *kwds) -{ - unsigned int width; - unsigned int height; - double dpi; - int debug = 0; - - if (!PyArg_ParseTuple(args, "IId|i:RendererAgg", &width, &height, &dpi, &debug)) { - return -1; - } - - if (dpi <= 0.0) { - PyErr_SetString(PyExc_ValueError, "dpi must be positive"); - return -1; - } - - if (width >= 1 << 16 || height >= 1 << 16) { - PyErr_Format( - PyExc_ValueError, - "Image size of %dx%d pixels is too large. " - "It must be less than 2^16 in each direction.", - width, height); - return -1; + agg::rgba face = rgbFace.cast(); + if (!rgbFace.is_none()) { + if (gc.forced_alpha || rgbFace.cast().size() == 3) { + face.a = gc.alpha; + } } - CALL_CPP_INIT("RendererAgg", self->x = new RendererAgg(width, height, dpi)) - - return 0; -} - -static void PyRendererAgg_dealloc(PyRendererAgg *self) -{ - delete self->x; - Py_TYPE(self)->tp_free((PyObject *)self); + self->draw_path(gc, path, trans, face); } -static PyObject *PyRendererAgg_draw_path(PyRendererAgg *self, PyObject *args) +static void +PyRendererAgg_draw_text_image(RendererAgg *self, + py::array_t image_obj, + double x, + double y, + double angle, + GCAgg &gc) { - GCAgg gc; - mpl::PathIterator path; - agg::trans_affine trans; - PyObject *faceobj = NULL; - agg::rgba face; - - if (!PyArg_ParseTuple(args, - "O&O&O&|O:draw_path", - &convert_gcagg, - &gc, - &convert_path, - &path, - &convert_trans_affine, - &trans, - &faceobj)) { - return NULL; - } + // TODO: This really shouldn't be mutable, but Agg's renderer buffers aren't const. + auto image = image_obj.mutable_unchecked<2>(); - if (!convert_face(faceobj, gc, &face)) { - return NULL; - } - - CALL_CPP("draw_path", (self->x->draw_path(gc, path, trans, face))); - - Py_RETURN_NONE; + self->draw_text_image(gc, image, x, y, angle); } -static PyObject *PyRendererAgg_draw_text_image(PyRendererAgg *self, PyObject *args) +static void +PyRendererAgg_draw_markers(RendererAgg *self, + GCAgg &gc, + mpl::PathIterator marker_path, + agg::trans_affine marker_path_trans, + mpl::PathIterator path, + agg::trans_affine trans, + py::object rgbFace) { - numpy::array_view image; - double x; - double y; - double angle; - GCAgg gc; - - if (!PyArg_ParseTuple(args, - "O&dddO&:draw_text_image", - &image.converter_contiguous, - &image, - &x, - &y, - &angle, - &convert_gcagg, - &gc)) { - return NULL; + agg::rgba face = rgbFace.cast(); + if (!rgbFace.is_none()) { + if (gc.forced_alpha || rgbFace.cast().size() == 3) { + face.a = gc.alpha; + } } - CALL_CPP("draw_text_image", (self->x->draw_text_image(gc, image, x, y, angle))); - - Py_RETURN_NONE; + self->draw_markers(gc, marker_path, marker_path_trans, path, trans, face); } -PyObject *PyRendererAgg_draw_markers(PyRendererAgg *self, PyObject *args) +static void +PyRendererAgg_draw_image(RendererAgg *self, + GCAgg &gc, + double x, + double y, + py::array_t image_obj) { - GCAgg gc; - mpl::PathIterator marker_path; - agg::trans_affine marker_path_trans; - mpl::PathIterator path; - agg::trans_affine trans; - PyObject *faceobj = NULL; - agg::rgba face; - - if (!PyArg_ParseTuple(args, - "O&O&O&O&O&|O:draw_markers", - &convert_gcagg, - &gc, - &convert_path, - &marker_path, - &convert_trans_affine, - &marker_path_trans, - &convert_path, - &path, - &convert_trans_affine, - &trans, - &faceobj)) { - return NULL; - } - - if (!convert_face(faceobj, gc, &face)) { - return NULL; - } - - CALL_CPP("draw_markers", - (self->x->draw_markers(gc, marker_path, marker_path_trans, path, trans, face))); - - Py_RETURN_NONE; -} - -static PyObject *PyRendererAgg_draw_image(PyRendererAgg *self, PyObject *args) -{ - GCAgg gc; - double x; - double y; - numpy::array_view image; - - if (!PyArg_ParseTuple(args, - "O&ddO&:draw_image", - &convert_gcagg, - &gc, - &x, - &y, - &image.converter_contiguous, - &image)) { - return NULL; - } + // TODO: This really shouldn't be mutable, but Agg's renderer buffers aren't const. + auto image = image_obj.mutable_unchecked<3>(); x = mpl_round(x); y = mpl_round(y); gc.alpha = 1.0; - CALL_CPP("draw_image", (self->x->draw_image(gc, x, y, image))); - - Py_RETURN_NONE; -} - -static PyObject * -PyRendererAgg_draw_path_collection(PyRendererAgg *self, PyObject *args) -{ - GCAgg gc; - agg::trans_affine master_transform; - mpl::PathGenerator paths; - numpy::array_view transforms; - numpy::array_view offsets; - agg::trans_affine offset_trans; - numpy::array_view facecolors; - numpy::array_view edgecolors; - numpy::array_view linewidths; - DashesVector dashes; - numpy::array_view antialiaseds; - PyObject *ignored; - PyObject *offset_position; // offset position is no longer used - - if (!PyArg_ParseTuple(args, - "O&O&O&O&O&O&O&O&O&O&O&OO:draw_path_collection", - &convert_gcagg, - &gc, - &convert_trans_affine, - &master_transform, - &convert_pathgen, - &paths, - &convert_transforms, - &transforms, - &convert_points, - &offsets, - &convert_trans_affine, - &offset_trans, - &convert_colors, - &facecolors, - &convert_colors, - &edgecolors, - &linewidths.converter, - &linewidths, - &convert_dashes_vector, - &dashes, - &antialiaseds.converter, - &antialiaseds, - &ignored, - &offset_position)) { - return NULL; - } - - CALL_CPP("draw_path_collection", - (self->x->draw_path_collection(gc, - master_transform, - paths, - transforms, - offsets, - offset_trans, - facecolors, - edgecolors, - linewidths, - dashes, - antialiaseds))); - - Py_RETURN_NONE; -} - -static PyObject *PyRendererAgg_draw_quad_mesh(PyRendererAgg *self, PyObject *args) -{ - GCAgg gc; - agg::trans_affine master_transform; - unsigned int mesh_width; - unsigned int mesh_height; - numpy::array_view coordinates; - numpy::array_view offsets; - agg::trans_affine offset_trans; - numpy::array_view facecolors; - bool antialiased; - numpy::array_view edgecolors; - - if (!PyArg_ParseTuple(args, - "O&O&IIO&O&O&O&O&O&:draw_quad_mesh", - &convert_gcagg, - &gc, - &convert_trans_affine, - &master_transform, - &mesh_width, - &mesh_height, - &coordinates.converter, - &coordinates, - &convert_points, - &offsets, - &convert_trans_affine, - &offset_trans, - &convert_colors, - &facecolors, - &convert_bool, - &antialiased, - &convert_colors, - &edgecolors)) { - return NULL; - } - - CALL_CPP("draw_quad_mesh", - (self->x->draw_quad_mesh(gc, - master_transform, - mesh_width, - mesh_height, - coordinates, - offsets, - offset_trans, - facecolors, - antialiased, - edgecolors))); - - Py_RETURN_NONE; -} - -static PyObject * -PyRendererAgg_draw_gouraud_triangles(PyRendererAgg *self, PyObject *args) -{ - GCAgg gc; - numpy::array_view points; - numpy::array_view colors; - agg::trans_affine trans; - - if (!PyArg_ParseTuple(args, - "O&O&O&O&|O:draw_gouraud_triangles", - &convert_gcagg, - &gc, - &points.converter, - &points, - &colors.converter, - &colors, - &convert_trans_affine, - &trans)) { - return NULL; - } - if (points.shape(0) && !check_trailing_shape(points, "points", 3, 2)) { - return NULL; - } - if (colors.shape(0) && !check_trailing_shape(colors, "colors", 3, 4)) { - return NULL; - } - if (points.shape(0) != colors.shape(0)) { - PyErr_Format(PyExc_ValueError, - "points and colors arrays must be the same length, got " - "%" NPY_INTP_FMT " points and %" NPY_INTP_FMT "colors", - points.shape(0), colors.shape(0)); - return NULL; - } - - CALL_CPP("draw_gouraud_triangles", self->x->draw_gouraud_triangles(gc, points, colors, trans)); - - Py_RETURN_NONE; + self->draw_image(gc, x, y, image); } -int PyRendererAgg_get_buffer(PyRendererAgg *self, Py_buffer *buf, int flags) +static void +PyRendererAgg_draw_path_collection(RendererAgg *self, + GCAgg &gc, + agg::trans_affine master_transform, + mpl::PathGenerator paths, + py::array_t transforms_obj, + py::array_t offsets_obj, + agg::trans_affine offset_trans, + py::array_t facecolors_obj, + py::array_t edgecolors_obj, + py::array_t linewidths_obj, + DashesVector dashes, + py::array_t antialiaseds_obj, + py::object Py_UNUSED(ignored_obj), + // offset position is no longer used + py::object Py_UNUSED(offset_position_obj)) { - Py_INCREF(self); - buf->obj = (PyObject *)self; - buf->buf = self->x->pixBuffer; - buf->len = (Py_ssize_t)self->x->get_width() * (Py_ssize_t)self->x->get_height() * 4; - buf->readonly = 0; - buf->format = (char *)"B"; - buf->ndim = 3; - self->shape[0] = self->x->get_height(); - self->shape[1] = self->x->get_width(); - self->shape[2] = 4; - buf->shape = self->shape; - self->strides[0] = self->x->get_width() * 4; - self->strides[1] = 4; - self->strides[2] = 1; - buf->strides = self->strides; - buf->suboffsets = NULL; - buf->itemsize = 1; - buf->internal = NULL; - - return 1; -} - -static PyObject *PyRendererAgg_clear(PyRendererAgg *self, PyObject *args) -{ - CALL_CPP("clear", self->x->clear()); - - Py_RETURN_NONE; -} - -static PyObject *PyRendererAgg_copy_from_bbox(PyRendererAgg *self, PyObject *args) -{ - agg::rect_d bbox; - BufferRegion *reg; - PyObject *regobj; - - if (!PyArg_ParseTuple(args, "O&:copy_from_bbox", &convert_rect, &bbox)) { - return 0; - } - - CALL_CPP("copy_from_bbox", (reg = self->x->copy_from_bbox(bbox))); - - regobj = PyBufferRegion_new(&PyBufferRegionType, NULL, NULL); - ((PyBufferRegion *)regobj)->x = reg; - - return regobj; + auto transforms = convert_transforms(transforms_obj); + auto offsets = convert_points(offsets_obj); + auto facecolors = convert_colors(facecolors_obj); + auto edgecolors = convert_colors(edgecolors_obj); + auto linewidths = linewidths_obj.unchecked<1>(); + auto antialiaseds = antialiaseds_obj.unchecked<1>(); + + self->draw_path_collection(gc, + master_transform, + paths, + transforms, + offsets, + offset_trans, + facecolors, + edgecolors, + linewidths, + dashes, + antialiaseds); } -static PyObject *PyRendererAgg_restore_region(PyRendererAgg *self, PyObject *args) +static void +PyRendererAgg_draw_quad_mesh(RendererAgg *self, + GCAgg &gc, + agg::trans_affine master_transform, + unsigned int mesh_width, + unsigned int mesh_height, + py::array_t coordinates_obj, + py::array_t offsets_obj, + agg::trans_affine offset_trans, + py::array_t facecolors_obj, + bool antialiased, + py::array_t edgecolors_obj) { - PyBufferRegion *regobj; - int xx1 = 0, yy1 = 0, xx2 = 0, yy2 = 0, x = 0, y = 0; - - if (!PyArg_ParseTuple(args, - "O!|iiiiii:restore_region", - &PyBufferRegionType, - ®obj, - &xx1, - &yy1, - &xx2, - &yy2, - &x, - &y)) { - return 0; - } - - if (PySequence_Size(args) == 1) { - CALL_CPP("restore_region", self->x->restore_region(*(regobj->x))); - } else { - CALL_CPP("restore_region", self->x->restore_region(*(regobj->x), xx1, yy1, xx2, yy2, x, y)); - } - - Py_RETURN_NONE; + auto coordinates = coordinates_obj.mutable_unchecked<3>(); + auto offsets = convert_points(offsets_obj); + auto facecolors = convert_colors(facecolors_obj); + auto edgecolors = convert_colors(edgecolors_obj); + + self->draw_quad_mesh(gc, + master_transform, + mesh_width, + mesh_height, + coordinates, + offsets, + offset_trans, + facecolors, + antialiased, + edgecolors); } -static PyTypeObject *PyRendererAgg_init_type() +static void +PyRendererAgg_draw_gouraud_triangles(RendererAgg *self, + GCAgg &gc, + py::array_t points_obj, + py::array_t colors_obj, + agg::trans_affine trans) { - static PyMethodDef methods[] = { - {"draw_path", (PyCFunction)PyRendererAgg_draw_path, METH_VARARGS, NULL}, - {"draw_markers", (PyCFunction)PyRendererAgg_draw_markers, METH_VARARGS, NULL}, - {"draw_text_image", (PyCFunction)PyRendererAgg_draw_text_image, METH_VARARGS, NULL}, - {"draw_image", (PyCFunction)PyRendererAgg_draw_image, METH_VARARGS, NULL}, - {"draw_path_collection", (PyCFunction)PyRendererAgg_draw_path_collection, METH_VARARGS, NULL}, - {"draw_quad_mesh", (PyCFunction)PyRendererAgg_draw_quad_mesh, METH_VARARGS, NULL}, - {"draw_gouraud_triangles", (PyCFunction)PyRendererAgg_draw_gouraud_triangles, METH_VARARGS, NULL}, + auto points = points_obj.unchecked<3>(); + auto colors = colors_obj.unchecked<3>(); - {"clear", (PyCFunction)PyRendererAgg_clear, METH_NOARGS, NULL}, - - {"copy_from_bbox", (PyCFunction)PyRendererAgg_copy_from_bbox, METH_VARARGS, NULL}, - {"restore_region", (PyCFunction)PyRendererAgg_restore_region, METH_VARARGS, NULL}, - {NULL} - }; - - static PyBufferProcs buffer_procs; - buffer_procs.bf_getbuffer = (getbufferproc)PyRendererAgg_get_buffer; - - PyRendererAggType.tp_name = "matplotlib.backends._backend_agg.RendererAgg"; - PyRendererAggType.tp_basicsize = sizeof(PyRendererAgg); - PyRendererAggType.tp_dealloc = (destructor)PyRendererAgg_dealloc; - PyRendererAggType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; - PyRendererAggType.tp_methods = methods; - PyRendererAggType.tp_init = (initproc)PyRendererAgg_init; - PyRendererAggType.tp_new = PyRendererAgg_new; - PyRendererAggType.tp_as_buffer = &buffer_procs; - - return &PyRendererAggType; + self->draw_gouraud_triangles(gc, points, colors, trans); } -static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "_backend_agg" }; - -PyMODINIT_FUNC PyInit__backend_agg(void) +PYBIND11_MODULE(_backend_agg, m) { - import_array(); - PyObject *m; - if (!(m = PyModule_Create(&moduledef)) - || prepare_and_add_type(PyRendererAgg_init_type(), m) - // BufferRegion is not constructible from Python, thus not added to the module. - || PyType_Ready(PyBufferRegion_init_type()) - ) { - Py_XDECREF(m); - return NULL; - } - return m; + py::class_(m, "RendererAgg", py::buffer_protocol()) + .def(py::init(), + "width"_a, "height"_a, "dpi"_a) + + .def("draw_path", &PyRendererAgg_draw_path, + "gc"_a, "path"_a, "trans"_a, "face"_a = nullptr) + .def("draw_markers", &PyRendererAgg_draw_markers, + "gc"_a, "marker_path"_a, "marker_path_trans"_a, "path"_a, "trans"_a, + "face"_a = nullptr) + .def("draw_text_image", &PyRendererAgg_draw_text_image, + "image"_a, "x"_a, "y"_a, "angle"_a, "gc"_a) + .def("draw_image", &PyRendererAgg_draw_image, + "gc"_a, "x"_a, "y"_a, "image"_a) + .def("draw_path_collection", &PyRendererAgg_draw_path_collection, + "gc"_a, "master_transform"_a, "paths"_a, "transforms"_a, "offsets"_a, + "offset_trans"_a, "facecolors"_a, "edgecolors"_a, "linewidths"_a, + "dashes"_a, "antialiaseds"_a, "ignored"_a, "offset_position"_a) + .def("draw_quad_mesh", &PyRendererAgg_draw_quad_mesh, + "gc"_a, "master_transform"_a, "mesh_width"_a, "mesh_height"_a, + "coordinates"_a, "offsets"_a, "offset_trans"_a, "facecolors"_a, + "antialiased"_a, "edgecolors"_a) + .def("draw_gouraud_triangles", &PyRendererAgg_draw_gouraud_triangles, + "gc"_a, "points"_a, "colors"_a, "trans"_a = nullptr) + + .def("clear", &RendererAgg::clear) + + .def("copy_from_bbox", &RendererAgg::copy_from_bbox, + "bbox"_a) + .def("restore_region", + py::overload_cast(&RendererAgg::restore_region), + "region"_a) + .def("restore_region", + py::overload_cast(&RendererAgg::restore_region), + "region"_a, "xx1"_a, "yy1"_a, "xx2"_a, "yy2"_a, "x"_a, "y"_a) + + .def_buffer([](RendererAgg *renderer) -> py::buffer_info { + std::vector shape { + renderer->get_height(), + renderer->get_width(), + 4 + }; + std::vector strides { + renderer->get_width() * 4, + 4, + 1 + }; + return py::buffer_info(renderer->pixBuffer, shape, strides); + }); + + py::class_(m, "BufferRegion", py::buffer_protocol()) + // BufferRegion is not constructible from Python, thus no py::init is added. + .def("set_x", &PyBufferRegion_set_x) + .def("set_y", &PyBufferRegion_set_y) + .def("get_extents", &PyBufferRegion_get_extents) + .def_buffer([](BufferRegion *buffer) -> py::buffer_info { + std::vector shape { + buffer->get_height(), + buffer->get_width(), + 4 + }; + std::vector strides { + buffer->get_width() * 4, + 4, + 1 + }; + return py::buffer_info(buffer->get_data(), shape, strides); + }); } diff --git a/src/_c_internal_utils.cpp b/src/_c_internal_utils.cpp index 74bb97904f89..561cb303639c 100644 --- a/src/_c_internal_utils.cpp +++ b/src/_c_internal_utils.cpp @@ -33,7 +33,7 @@ namespace py = pybind11; using namespace pybind11::literals; static bool -mpl_display_is_valid(void) +mpl_xdisplay_is_valid(void) { #ifdef __linux__ void* libX11; @@ -57,6 +57,19 @@ mpl_display_is_valid(void) return true; } } + return false; +#else + return true; +#endif +} + +static bool +mpl_display_is_valid(void) +{ +#ifdef __linux__ + if (mpl_xdisplay_is_valid()) { + return true; + } void* libwayland_client; if (getenv("WAYLAND_DISPLAY") && (libwayland_client = dlopen("libwayland-client.so.0", RTLD_LAZY))) { @@ -194,6 +207,16 @@ PYBIND11_MODULE(_c_internal_utils, m) succeeds, or $WAYLAND_DISPLAY is set and wl_display_connect(NULL) succeeds. + On other platforms, always returns True.)"""); + m.def( + "xdisplay_is_valid", &mpl_xdisplay_is_valid, + R"""( -- + Check whether the current X11 display is valid. + + On Linux, returns True if either $DISPLAY is set and XOpenDisplay(NULL) + succeeds. Use this function if you need to specifically check for X11 + only (e.g., for Tkinter). + On other platforms, always returns True.)"""); m.def( "Win32_GetCurrentProcessExplicitAppUserModelID", diff --git a/src/_image_resample.h b/src/_image_resample.h index ddf1a4050325..dbc6171f3ec2 100644 --- a/src/_image_resample.h +++ b/src/_image_resample.h @@ -712,6 +712,7 @@ void resample( using renderer_t = agg::renderer_base; using rasterizer_t = agg::rasterizer_scanline_aa; + using scanline_t = agg::scanline32_u8; using reflect_t = agg::wrap_mode_reflect; using image_accessor_t = agg::image_accessor_wrap; @@ -739,7 +740,7 @@ void resample( span_alloc_t span_alloc; rasterizer_t rasterizer; - agg::scanline_u8 scanline; + scanline_t scanline; span_conv_alpha_t conv_alpha(params.alpha); diff --git a/src/_image_wrapper.cpp b/src/_image_wrapper.cpp index 856dcf4ea3ce..0095f52e5997 100644 --- a/src/_image_wrapper.cpp +++ b/src/_image_wrapper.cpp @@ -2,7 +2,7 @@ #include #include "_image_resample.h" -#include "py_converters_11.h" +#include "py_converters.h" namespace py = pybind11; using namespace pybind11::literals; diff --git a/src/_macosx.m b/src/_macosx.m index fda928536ab5..09838eccaf98 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -1236,7 +1236,7 @@ -(void)drawRect:(NSRect)rect CGContextRef cr = [[NSGraphicsContext currentContext] CGContext]; if (!(renderer = PyObject_CallMethod(canvas, "get_renderer", "")) - || !(renderer_buffer = PyObject_GetAttrString(renderer, "_renderer"))) { + || !(renderer_buffer = PyObject_CallMethod(renderer, "buffer_rgba", ""))) { PyErr_Print(); goto exit; } diff --git a/src/_path.h b/src/_path.h index 7f17d0bc2933..f5c06e4a6a15 100644 --- a/src/_path.h +++ b/src/_path.h @@ -18,7 +18,6 @@ #include "path_converters.h" #include "_backend_agg_basic_types.h" -#include "numpy_cpp.h" const size_t NUM_VERTICES[] = { 1, 1, 1, 2, 3 }; @@ -245,8 +244,7 @@ inline void points_in_path(PointArray &points, typedef agg::conv_curve curve_t; typedef agg::conv_contour contour_t; - size_t i; - for (i = 0; i < safe_first_shape(points); ++i) { + for (auto i = 0; i < safe_first_shape(points); ++i) { result[i] = false; } @@ -270,10 +268,11 @@ template inline bool point_in_path( double x, double y, const double r, PathIterator &path, agg::trans_affine &trans) { - npy_intp shape[] = {1, 2}; - numpy::array_view points(shape); - points(0, 0) = x; - points(0, 1) = y; + py::ssize_t shape[] = {1, 2}; + py::array_t points_arr(shape); + *points_arr.mutable_data(0, 0) = x; + *points_arr.mutable_data(0, 1) = y; + auto points = points_arr.mutable_unchecked<2>(); int result[1]; result[0] = 0; @@ -292,10 +291,11 @@ inline bool point_on_path( typedef agg::conv_curve curve_t; typedef agg::conv_stroke stroke_t; - npy_intp shape[] = {1, 2}; - numpy::array_view points(shape); - points(0, 0) = x; - points(0, 1) = y; + py::ssize_t shape[] = {1, 2}; + py::array_t points_arr(shape); + *points_arr.mutable_data(0, 0) = x; + *points_arr.mutable_data(0, 1) = y; + auto points = points_arr.mutable_unchecked<2>(); int result[1]; result[0] = 0; @@ -382,20 +382,19 @@ void get_path_collection_extents(agg::trans_affine &master_transform, throw std::runtime_error("Offsets array must have shape (N, 2)"); } - size_t Npaths = paths.size(); - size_t Noffsets = safe_first_shape(offsets); - size_t N = std::max(Npaths, Noffsets); - size_t Ntransforms = std::min(safe_first_shape(transforms), N); - size_t i; + auto Npaths = paths.size(); + auto Noffsets = safe_first_shape(offsets); + auto N = std::max(Npaths, Noffsets); + auto Ntransforms = std::min(safe_first_shape(transforms), N); agg::trans_affine trans; reset_limits(extent); - for (i = 0; i < N; ++i) { + for (auto i = 0; i < N; ++i) { typename PathGenerator::path_iterator path(paths(i % Npaths)); if (Ntransforms) { - size_t ti = i % Ntransforms; + py::ssize_t ti = i % Ntransforms; trans = agg::trans_affine(transforms(ti, 0, 0), transforms(ti, 1, 0), transforms(ti, 0, 1), @@ -429,24 +428,23 @@ void point_in_path_collection(double x, bool filled, std::vector &result) { - size_t Npaths = paths.size(); + auto Npaths = paths.size(); if (Npaths == 0) { return; } - size_t Noffsets = safe_first_shape(offsets); - size_t N = std::max(Npaths, Noffsets); - size_t Ntransforms = std::min(safe_first_shape(transforms), N); - size_t i; + auto Noffsets = safe_first_shape(offsets); + auto N = std::max(Npaths, Noffsets); + auto Ntransforms = std::min(safe_first_shape(transforms), N); agg::trans_affine trans; - for (i = 0; i < N; ++i) { + for (auto i = 0; i < N; ++i) { typename PathGenerator::path_iterator path = paths(i % Npaths); if (Ntransforms) { - size_t ti = i % Ntransforms; + auto ti = i % Ntransforms; trans = agg::trans_affine(transforms(ti, 0, 0), transforms(ti, 1, 0), transforms(ti, 0, 1), @@ -1005,7 +1003,7 @@ void convert_path_to_polygons(PathIterator &path, template void -__cleanup_path(VertexSource &source, std::vector &vertices, std::vector &codes) +__cleanup_path(VertexSource &source, std::vector &vertices, std::vector &codes) { unsigned code; double x, y; @@ -1013,7 +1011,7 @@ __cleanup_path(VertexSource &source, std::vector &vertices, std::vector< code = source.vertex(&x, &y); vertices.push_back(x); vertices.push_back(y); - codes.push_back((npy_uint8)code); + codes.push_back(static_cast(code)); } while (code != agg::path_cmd_stop); } @@ -1224,17 +1222,15 @@ bool convert_to_string(PathIterator &path, } template -bool is_sorted_and_has_non_nan(PyArrayObject *array) +bool is_sorted_and_has_non_nan(py::array_t array) { - char* ptr = PyArray_BYTES(array); - npy_intp size = PyArray_DIM(array, 0), - stride = PyArray_STRIDE(array, 0); + auto size = array.shape(0); using limits = std::numeric_limits; T last = limits::has_infinity ? -limits::infinity() : limits::min(); bool found_non_nan = false; - for (npy_intp i = 0; i < size; ++i, ptr += stride) { - T current = *(T*)ptr; + for (auto i = 0; i < size; ++i) { + T current = *array.data(i); // The following tests !isnan(current), but also works for integral // types. (The isnan(IntegralType) overload is absent on MSVC.) if (current == current) { diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index b4eb5d19177f..7a7cb343b8e7 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -7,14 +7,11 @@ #include #include -#include "numpy_cpp.h" - #include "_path.h" #include "_backend_agg_basic_types.h" #include "py_adaptors.h" #include "py_converters.h" -#include "py_converters_11.h" namespace py = pybind11; using namespace pybind11::literals; @@ -44,17 +41,9 @@ static py::array_t Py_points_in_path(py::array_t points_obj, double r, mpl::PathIterator path, agg::trans_affine trans) { - numpy::array_view points; - - if (!convert_points(points_obj.ptr(), &points)) { - throw py::error_already_set(); - } + auto points = convert_points(points_obj); - if (!check_trailing_shape(points, "points", 2)) { - throw py::error_already_set(); - } - - py::ssize_t dims[] = { static_cast(points.size()) }; + py::ssize_t dims[] = { points.shape(0) }; py::array_t results(dims); auto results_mutable = results.mutable_unchecked<1>(); @@ -123,24 +112,15 @@ Py_update_path_extents(mpl::PathIterator path, agg::trans_affine trans, static py::tuple Py_get_path_collection_extents(agg::trans_affine master_transform, - py::object paths_obj, py::object transforms_obj, - py::object offsets_obj, agg::trans_affine offset_trans) + mpl::PathGenerator paths, + py::array_t transforms_obj, + py::array_t offsets_obj, + agg::trans_affine offset_trans) { - mpl::PathGenerator paths; - numpy::array_view transforms; - numpy::array_view offsets; + auto transforms = convert_transforms(transforms_obj); + auto offsets = convert_points(offsets_obj); extent_limits e; - if (!convert_pathgen(paths_obj.ptr(), &paths)) { - throw py::error_already_set(); - } - if (!convert_transforms(transforms_obj.ptr(), &transforms)) { - throw py::error_already_set(); - } - if (!convert_points(offsets_obj.ptr(), &offsets)) { - throw py::error_already_set(); - } - get_path_collection_extents( master_transform, paths, transforms, offsets, offset_trans, e); @@ -161,25 +141,15 @@ Py_get_path_collection_extents(agg::trans_affine master_transform, static py::object Py_point_in_path_collection(double x, double y, double radius, - agg::trans_affine master_transform, py::object paths_obj, - py::object transforms_obj, py::object offsets_obj, + agg::trans_affine master_transform, mpl::PathGenerator paths, + py::array_t transforms_obj, + py::array_t offsets_obj, agg::trans_affine offset_trans, bool filled) { - mpl::PathGenerator paths; - numpy::array_view transforms; - numpy::array_view offsets; + auto transforms = convert_transforms(transforms_obj); + auto offsets = convert_points(offsets_obj); std::vector result; - if (!convert_pathgen(paths_obj.ptr(), &paths)) { - throw py::error_already_set(); - } - if (!convert_transforms(transforms_obj.ptr(), &transforms)) { - throw py::error_already_set(); - } - if (!convert_points(offsets_obj.ptr(), &offsets)) { - throw py::error_already_set(); - } - point_in_path_collection(x, y, radius, master_transform, paths, transforms, offsets, offset_trans, filled, result); @@ -211,9 +181,7 @@ Py_affine_transform(py::array_t(); - if(!check_trailing_shape(vertices, "vertices", 2)) { - throw py::error_already_set(); - } + check_trailing_shape(vertices, "vertices", 2); py::ssize_t dims[] = { vertices.shape(0), 2 }; py::array_t result(dims); @@ -237,13 +205,9 @@ Py_affine_transform(py::array_t bboxes_obj) { - numpy::array_view bboxes; - - if (!convert_bboxes(bboxes_obj.ptr(), &bboxes)) { - throw py::error_already_set(); - } + auto bboxes = convert_bboxes(bboxes_obj); return count_bboxes_overlapping_bbox(bbox, bboxes); } @@ -298,7 +262,7 @@ Py_cleanup_path(mpl::PathIterator path, agg::trans_affine trans, bool remove_nan bool do_clip = (clip_rect.x1 < clip_rect.x2 && clip_rect.y1 < clip_rect.y2); std::vector vertices; - std::vector codes; + std::vector codes; cleanup_path(path, trans, remove_nans, do_clip, clip_rect, snap_mode, stroke_width, *simplify, return_curves, sketch, vertices, codes); @@ -381,54 +345,31 @@ Py_is_sorted_and_has_non_nan(py::object obj) { bool result; - PyArrayObject *array = (PyArrayObject *)PyArray_CheckFromAny( - obj.ptr(), NULL, 1, 1, NPY_ARRAY_NOTSWAPPED, NULL); - - if (array == NULL) { - throw py::error_already_set(); + py::array array = py::array::ensure(obj); + if (array.ndim() != 1) { + throw std::invalid_argument("array must be 1D"); } + auto dtype = array.dtype(); /* Handle just the most common types here, otherwise coerce to double */ - switch (PyArray_TYPE(array)) { - case NPY_INT: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_LONG: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_LONGLONG: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_FLOAT: - result = is_sorted_and_has_non_nan(array); - break; - case NPY_DOUBLE: - result = is_sorted_and_has_non_nan(array); - break; - default: - Py_DECREF(array); - array = (PyArrayObject *)PyArray_FromObject(obj.ptr(), NPY_DOUBLE, 1, 1); - if (array == NULL) { - throw py::error_already_set(); - } - result = is_sorted_and_has_non_nan(array); + if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else if (dtype.equal(py::dtype::of())) { + result = is_sorted_and_has_non_nan(array); + } else { + array = py::array_t::ensure(obj); + result = is_sorted_and_has_non_nan(array); } - Py_DECREF(array); - return result; } PYBIND11_MODULE(_path, m) { - auto ia = [m]() -> const void* { - import_array(); - return &m; - }; - if (ia() == NULL) { - throw py::error_already_set(); - } - m.def("point_in_path", &Py_point_in_path, "x"_a, "y"_a, "radius"_a, "path"_a, "trans"_a); m.def("points_in_path", &Py_points_in_path, diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 9791dc7e2e06..4358646beede 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -4,7 +4,6 @@ #include #include "ft2font.h" -#include "numpy/arrayobject.h" #include #include @@ -955,14 +954,6 @@ PyFT2Font_fname(PyFT2Font *self) PYBIND11_MODULE(ft2font, m) { - auto ia = [m]() -> const void* { - import_array(); - return &m; - }; - if (ia() == NULL) { - throw py::error_already_set(); - } - if (FT_Init_FreeType(&_ft2Library)) { // initialize library throw std::runtime_error("Could not initialize the freetype2 library"); } diff --git a/src/meson.build b/src/meson.build index 4edd8451aad2..d2bc9e4afccd 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,42 +1,3 @@ -# NumPy include directory - needed in all submodules -# The try-except is needed because when things are split across drives on Windows, there -# is no relative path and an exception gets raised. There may be other such cases, so add -# a catch-all and switch to an absolute path. Relative paths are needed when for example -# a virtualenv is placed inside the source tree; Meson rejects absolute paths to places -# inside the source tree. -# For cross-compilation it is often not possible to run the Python interpreter in order -# to retrieve numpy's include directory. It can be specified in the cross file instead: -# -# [properties] -# numpy-include-dir = /abspath/to/host-pythons/site-packages/numpy/core/include -# -# This uses the path as is, and avoids running the interpreter. -incdir_numpy = meson.get_external_property('numpy-include-dir', 'not-given') -if incdir_numpy == 'not-given' - incdir_numpy = run_command(py3, - [ - '-c', - '''import os -import numpy as np -try: - incdir = os.path.relpath(np.get_include()) -except Exception: - incdir = np.get_include() -print(incdir)''' - ], - check: true - ).stdout().strip() -endif -numpy_dep = declare_dependency( - compile_args: [ - '-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION', - # Allow NumPy's printf format specifiers in C++. - '-D__STDC_FORMAT_MACROS=1', - ], - include_directories: include_directories(incdir_numpy), - dependencies: py3_dep, -) - # For cross-compilation it is often not possible to run the Python interpreter in order # to retrieve the platform-specific /dev/null. It can be specified in the cross file # instead: @@ -73,11 +34,10 @@ extension_data = { '_backend_agg': { 'subdir': 'matplotlib/backends', 'sources': files( - 'py_converters.cpp', '_backend_agg.cpp', '_backend_agg_wrapper.cpp', ), - 'dependencies': [agg_dep, numpy_dep, freetype_dep], + 'dependencies': [agg_dep, freetype_dep, pybind11_dep], }, '_c_internal_utils': { 'subdir': 'matplotlib', @@ -91,10 +51,9 @@ extension_data = { 'sources': files( 'ft2font.cpp', 'ft2font_wrapper.cpp', - 'py_converters.cpp', ), 'dependencies': [ - freetype_dep, pybind11_dep, numpy_dep, agg_dep.partial_dependency(includes: true), + freetype_dep, pybind11_dep, agg_dep.partial_dependency(includes: true), ], 'cpp_args': [ '-DFREETYPE_BUILD_TYPE="@0@"'.format( @@ -106,7 +65,7 @@ extension_data = { 'subdir': 'matplotlib', 'sources': files( '_image_wrapper.cpp', - 'py_converters_11.cpp', + 'py_converters.cpp', ), 'dependencies': [ pybind11_dep, @@ -117,11 +76,9 @@ extension_data = { '_path': { 'subdir': 'matplotlib', 'sources': files( - 'py_converters.cpp', - 'py_converters_11.cpp', '_path_wrapper.cpp', ), - 'dependencies': [numpy_dep, agg_dep, pybind11_dep], + 'dependencies': [agg_dep, pybind11_dep], }, '_qhull': { 'subdir': 'matplotlib', @@ -161,16 +118,7 @@ extension_data = { } foreach ext, kwargs : extension_data - # Ensure that PY_ARRAY_UNIQUE_SYMBOL is uniquely defined for each extension. - unique_array_api = '-DPY_ARRAY_UNIQUE_SYMBOL=MPL_@0@_ARRAY_API'.format(ext.replace('.', '_')) - additions = { - 'c_args': [unique_array_api] + kwargs.get('c_args', []), - 'cpp_args': [unique_array_api] + kwargs.get('cpp_args', []), - } - py3.extension_module( - ext, - install: true, - kwargs: kwargs + additions) + py3.extension_module(ext, install: true, kwargs: kwargs) endforeach if get_option('macosx') and host_machine.system() == 'darwin' diff --git a/src/mplutils.h b/src/mplutils.h index 58eb32d190ec..95d9a2d9eb90 100644 --- a/src/mplutils.h +++ b/src/mplutils.h @@ -48,48 +48,70 @@ enum { CLOSEPOLY = 0x4f }; -inline int prepare_and_add_type(PyTypeObject *type, PyObject *module) -{ - if (PyType_Ready(type)) { - return -1; - } - char const* ptr = strrchr(type->tp_name, '.'); - if (!ptr) { - PyErr_SetString(PyExc_ValueError, "tp_name should be a qualified name"); - return -1; - } - if (PyModule_AddObject(module, ptr + 1, (PyObject *)type)) { - return -1; - } - return 0; -} - #ifdef __cplusplus // not for macosx.m // Check that array has shape (N, d1) or (N, d1, d2). We cast d1, d2 to longs // so that we don't need to access the NPY_INTP_FMT macro here. +#include +#include + +namespace py = pybind11; +using namespace pybind11::literals; template -inline bool check_trailing_shape(T array, char const* name, long d1) +inline void check_trailing_shape(T array, char const* name, long d1) { + if (array.ndim() != 2) { + throw py::value_error( + "Expected 2-dimensional array, got %d"_s.format(array.ndim())); + } + if (array.size() == 0) { + // Sometimes things come through as atleast_2d, etc., but they're empty, so + // don't bother enforcing the trailing shape. + return; + } if (array.shape(1) != d1) { - PyErr_Format(PyExc_ValueError, - "%s must have shape (N, %ld), got (%ld, %ld)", - name, d1, array.shape(0), array.shape(1)); - return false; + throw py::value_error( + "%s must have shape (N, %d), got (%d, %d)"_s.format( + name, d1, array.shape(0), array.shape(1))); } - return true; } template -inline bool check_trailing_shape(T array, char const* name, long d1, long d2) +inline void check_trailing_shape(T array, char const* name, long d1, long d2) { + if (array.ndim() != 3) { + throw py::value_error( + "Expected 3-dimensional array, got %d"_s.format(array.ndim())); + } + if (array.size() == 0) { + // Sometimes things come through as atleast_3d, etc., but they're empty, so + // don't bother enforcing the trailing shape. + return; + } if (array.shape(1) != d1 || array.shape(2) != d2) { - PyErr_Format(PyExc_ValueError, - "%s must have shape (N, %ld, %ld), got (%ld, %ld, %ld)", - name, d1, d2, array.shape(0), array.shape(1), array.shape(2)); - return false; + throw py::value_error( + "%s must have shape (N, %d, %d), got (%d, %d, %d)"_s.format( + name, d1, d2, array.shape(0), array.shape(1), array.shape(2))); + } +} + +/* In most cases, code should use safe_first_shape(obj) instead of obj.shape(0), since + safe_first_shape(obj) == 0 when any dimension is 0. */ +template +py::ssize_t +safe_first_shape(const py::detail::unchecked_reference &a) +{ + bool empty = (ND == 0); + for (py::ssize_t i = 0; i < ND; i++) { + if (a.shape(i) == 0) { + empty = true; + } + } + if (empty) { + return 0; + } else { + return a.shape(0); } - return true; } #endif diff --git a/src/numpy_cpp.h b/src/numpy_cpp.h deleted file mode 100644 index 6165789b7603..000000000000 --- a/src/numpy_cpp.h +++ /dev/null @@ -1,582 +0,0 @@ -/* -*- mode: c++; c-basic-offset: 4 -*- */ - -#ifndef MPL_NUMPY_CPP_H -#define MPL_NUMPY_CPP_H -#define PY_SSIZE_T_CLEAN -/*************************************************************************** - * This file is based on original work by Mark Wiebe, available at: - * - * http://github.com/mwiebe/numpy-cpp - * - * However, the needs of matplotlib wrappers, such as treating an - * empty array as having the correct dimensions, have made this rather - * matplotlib-specific, so it's no longer compatible with the - * original. - */ - -#ifdef _POSIX_C_SOURCE -# undef _POSIX_C_SOURCE -#endif -#ifndef _AIX -#ifdef _XOPEN_SOURCE -# undef _XOPEN_SOURCE -#endif -#endif - -// Prevent multiple conflicting definitions of swab from stdlib.h and unistd.h -#if defined(__sun) || defined(sun) -#if defined(_XPG4) -#undef _XPG4 -#endif -#if defined(_XPG3) -#undef _XPG3 -#endif -#endif - -#include -#include - -#include "py_exceptions.h" - -#include - -namespace numpy -{ - -// Type traits for the NumPy types -template -struct type_num_of; - -/* Be careful with bool arrays as python has sizeof(npy_bool) == 1, but it is - * not always the case that sizeof(bool) == 1. Using the array_view_accessors - * is always fine regardless of sizeof(bool), so do this rather than using - * array.data() and pointer arithmetic which will not work correctly if - * sizeof(bool) != 1. */ -template <> struct type_num_of -{ - enum { - value = NPY_BOOL - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_BYTE - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_UBYTE - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_SHORT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_USHORT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_INT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_UINT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_LONG - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_ULONG - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_LONGLONG - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_ULONGLONG - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_FLOAT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_DOUBLE - }; -}; -#if NPY_LONGDOUBLE != NPY_DOUBLE -template <> -struct type_num_of -{ - enum { - value = NPY_LONGDOUBLE - }; -}; -#endif -template <> -struct type_num_of -{ - enum { - value = NPY_CFLOAT - }; -}; -template <> -struct type_num_of > -{ - enum { - value = NPY_CFLOAT - }; -}; -template <> -struct type_num_of -{ - enum { - value = NPY_CDOUBLE - }; -}; -template <> -struct type_num_of > -{ - enum { - value = NPY_CDOUBLE - }; -}; -#if NPY_CLONGDOUBLE != NPY_CDOUBLE -template <> -struct type_num_of -{ - enum { - value = NPY_CLONGDOUBLE - }; -}; -template <> -struct type_num_of > -{ - enum { - value = NPY_CLONGDOUBLE - }; -}; -#endif -template <> -struct type_num_of -{ - enum { - value = NPY_OBJECT - }; -}; -template -struct type_num_of -{ - enum { - value = type_num_of::value - }; -}; -template -struct type_num_of -{ - enum { - value = type_num_of::value - }; -}; - -template -struct is_const -{ - enum { - value = false - }; -}; -template -struct is_const -{ - enum { - value = true - }; -}; - -namespace detail -{ -template