diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index a19b6d2346e3..f3595d2b7865 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -82,6 +82,8 @@ body: options: - pip - conda + - pixi + - uv - Linux package manager - from source (.tar.gz) - git checkout diff --git a/.github/workflows/cibuildwheel.yml b/.github/workflows/cibuildwheel.yml index fececb0dfc40..fd8dc309a61c 100644 --- a/.github/workflows/cibuildwheel.yml +++ b/.github/workflows/cibuildwheel.yml @@ -41,12 +41,12 @@ jobs: SDIST_NAME: ${{ steps.sdist.outputs.SDIST_NAME }} steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 name: Install Python with: python-version: '3.11' @@ -72,7 +72,7 @@ jobs: run: twine check dist/* - name: Upload sdist result - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: cibw-sdist path: dist/*.tar.gz @@ -127,21 +127,23 @@ jobs: - os: ubuntu-24.04-arm cibw_archs: "aarch64" - os: windows-latest - cibw_archs: "auto64" - - os: macos-13 + cibw_archs: "AMD64" + - os: windows-11-arm + cibw_archs: "ARM64" + - os: macos-15-intel cibw_archs: "x86_64" - os: macos-14 cibw_archs: "arm64" steps: - name: Download sdist - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 with: name: cibw-sdist path: dist/ - name: Build wheels for CPython 3.14 - uses: pypa/cibuildwheel@5f22145df44122af0f5a201f93cf0207171beca7 # v3.0.0 + uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -149,13 +151,9 @@ jobs: CIBW_ENABLE: "cpython-freethreading cpython-prerelease" CIBW_ARCHS: ${{ matrix.cibw_archs }} CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 - CIBW_BEFORE_TEST: >- - python -m pip install - --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple - --upgrade --pre --only-binary=:all: contourpy numpy pillow - name: Build wheels for CPython 3.13 - uses: pypa/cibuildwheel@5f22145df44122af0f5a201f93cf0207171beca7 # v3.0.0 + uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -164,7 +162,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.12 - uses: pypa/cibuildwheel@5f22145df44122af0f5a201f93cf0207171beca7 # v3.0.0 + uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -172,7 +170,7 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for CPython 3.11 - uses: pypa/cibuildwheel@5f22145df44122af0f5a201f93cf0207171beca7 # v3.0.0 + uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: @@ -180,46 +178,17 @@ jobs: CIBW_ARCHS: ${{ matrix.cibw_archs }} - name: Build wheels for PyPy - uses: pypa/cibuildwheel@5f22145df44122af0f5a201f93cf0207171beca7 # v3.0.0 + uses: pypa/cibuildwheel@298ed2fb2c105540f5ed055e8a6ad78d82dd3a7e # v3.3.1 with: package-dir: dist/${{ needs.build_sdist.outputs.SDIST_NAME }} env: CIBW_BUILD: "pp311-*" CIBW_ARCHS: ${{ matrix.cibw_archs }} CIBW_ENABLE: pypy - if: matrix.cibw_archs != 'aarch64' && matrix.os != 'windows-latest' + if: matrix.cibw_archs != 'aarch64' && matrix.os != 'windows-latest' && matrix.os != 'windows-11-arm' - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 with: name: cibw-wheels-${{ runner.os }}-${{ matrix.cibw_archs }} path: ./wheelhouse/*.whl if-no-files-found: error - - publish: - if: github.repository == 'matplotlib/matplotlib' && github.event_name == 'push' && github.ref_type == 'tag' - name: Upload release to PyPI - needs: [build_sdist, build_wheels] - runs-on: ubuntu-latest - environment: release - permissions: - id-token: write - attestations: write - contents: read - steps: - - name: Download packages - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - with: - pattern: cibw-* - path: dist - merge-multiple: true - - - name: Print out packages - run: ls dist - - - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0 - with: - subject-path: dist/matplotlib-* - - - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc # v1.12.4 diff --git a/.github/workflows/circleci.yml b/.github/workflows/circleci.yml index d61db3f14345..05a30eb032cb 100644 --- a/.github/workflows/circleci.yml +++ b/.github/workflows/circleci.yml @@ -11,7 +11,7 @@ jobs: steps: - name: GitHub Action step uses: - scientific-python/circleci-artifacts-redirector-action@7eafdb60666f57706a5525a2f5eb76224dc8779b # v1.1.0 + scientific-python/circleci-artifacts-redirector-action@5d358ff96e96429a5c64a969bb4a574555439f4f # v1.3.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} api-token: ${{ secrets.CIRCLECI_TOKEN }} @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest name: Post warnings/errors as review steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false @@ -41,7 +41,7 @@ jobs: - name: Set up reviewdog if: "${{ steps.fetch-artifacts.outputs.count != 0 }}" - uses: reviewdog/action-setup@e04ffabe3898a0af8d0fb1af00c188831c4b5893 # v1.3.2 + uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1.5.0 with: reviewdog_version: latest diff --git a/.github/workflows/clean_pr.yml b/.github/workflows/clean_pr.yml index fc9021c920c0..5107bb92fa1c 100644 --- a/.github/workflows/clean_pr.yml +++ b/.github/workflows/clean_pr.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: '0' persist-credentials: false diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d6d1eba02560..2aa1ce9f911d 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -27,12 +27,12 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 + uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 with: languages: ${{ matrix.language }} @@ -43,4 +43,4 @@ jobs: pip install --user -v . - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@181d5eefc20863364f96762470ba6f862bdef56b # v3.29.2 + uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 diff --git a/.github/workflows/cygwin.yml b/.github/workflows/cygwin.yml index 4a5b79c0538e..ae80020e08b7 100644 --- a/.github/workflows/cygwin.yml +++ b/.github/workflows/cygwin.yml @@ -79,12 +79,12 @@ jobs: - name: Fix line endings run: git config --global core.autocrlf input - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 persist-credentials: false - - uses: cygwin/cygwin-install-action@f61179d72284ceddc397ed07ddb444d82bf9e559 # v5 + - uses: cygwin/cygwin-install-action@f2009323764960f80959895c7bc3bb30210afe4d # v6 with: packages: >- ccache gcc-g++ gdb git graphviz libcairo-devel libffi-devel @@ -140,21 +140,21 @@ jobs: # FreeType build fails with bash, succeeds with dash - name: Cache pip - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: C:\cygwin\home\runneradmin\.cache\pip key: Cygwin-py3.${{ matrix.python-minor-version }}-pip-${{ hashFiles('requirements/*/*.txt') }} restore-keys: ${{ matrix.os }}-py3.${{ matrix.python-minor-version }}-pip- - name: Cache ccache - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: C:\cygwin\home\runneradmin\.ccache key: Cygwin-py3.${{ matrix.python-minor-version }}-ccache-${{ hashFiles('src/*') }} restore-keys: Cygwin-py3.${{ matrix.python-minor-version }}-ccache- - name: Cache Matplotlib - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: | C:\cygwin\home\runneradmin\.cache\matplotlib diff --git a/.github/workflows/do_not_merge.yml b/.github/workflows/do_not_merge.yml index d8664df9ba9a..0c263623942b 100644 --- a/.github/workflows/do_not_merge.yml +++ b/.github/workflows/do_not_merge.yml @@ -15,7 +15,8 @@ jobs: env: has_tag: >- ${{contains(github.event.pull_request.labels.*.name, 'status: needs comment/discussion') || - contains(github.event.pull_request.labels.*.name, 'status: waiting for other PR')}} + contains(github.event.pull_request.labels.*.name, 'status: waiting for other PR') || + contains(github.event.pull_request.labels.*.name, 'DO NOT MERGE') }} steps: - name: Check for label if: ${{'true' == env.has_tag}} @@ -23,6 +24,7 @@ jobs: echo "This PR cannot be merged because it has one of the following labels: " echo "* status: needs comment/discussion" echo "* status: waiting for other PR" + echo "* DO NOT MERGE" exit 1 - name: Allow merging if: ${{'false' == env.has_tag}} diff --git a/.github/workflows/good-first-issue.yml b/.github/workflows/good-first-issue.yml index cc15717e3351..5bb69db0595c 100644 --- a/.github/workflows/good-first-issue.yml +++ b/.github/workflows/good-first-issue.yml @@ -12,7 +12,7 @@ jobs: issues: write steps: - name: Add comment - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 # v4.0.0 + uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0 with: issue-number: ${{ github.event.issue.number }} body: | diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 8e2002353164..17c4922df054 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -10,6 +10,6 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0 + - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 with: sync-labels: true diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/linting.yml similarity index 69% rename from .github/workflows/reviewdog.yml rename to .github/workflows/linting.yml index bfad14923b82..bcc7d406043a 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/linting.yml @@ -10,10 +10,11 @@ jobs: name: precommit runs-on: ubuntu-latest steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + persist-credentials: false + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: "3.x" - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 @@ -26,12 +27,12 @@ jobs: permissions: checks: write steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: '3.11' @@ -39,7 +40,7 @@ jobs: run: pip3 install ruff - name: Set up reviewdog - uses: reviewdog/action-setup@e04ffabe3898a0af8d0fb1af00c188831c4b5893 # v1.3.9 + uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1.5.0 - name: Run ruff env: @@ -55,12 +56,12 @@ jobs: permissions: checks: write steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: '3.11' @@ -68,7 +69,7 @@ jobs: run: pip3 install -r requirements/testing/mypy.txt -r requirements/testing/all.txt - name: Set up reviewdog - uses: reviewdog/action-setup@e04ffabe3898a0af8d0fb1af00c188831c4b5893 # v1.3.9 + uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1.5.0 - name: Run mypy env: @@ -86,12 +87,12 @@ jobs: permissions: checks: write steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false - name: eslint - uses: reviewdog/action-eslint@2fee6dd72a5419ff4113f694e2068d2a03bb35dd # v1.33.2 + uses: reviewdog/action-eslint@556a3fdaf8b4201d4d74d406013386aa4f7dab96 # v1.34.0 with: filter_mode: nofilter github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/mypy-stubtest.yml b/.github/workflows/mypy-stubtest.yml index 92a67236fb9d..dad980b4fae9 100644 --- a/.github/workflows/mypy-stubtest.yml +++ b/.github/workflows/mypy-stubtest.yml @@ -12,17 +12,17 @@ jobs: permissions: checks: write steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: persist-credentials: false - name: Set up Python 3 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: '3.11' - name: Set up reviewdog - uses: reviewdog/action-setup@e04ffabe3898a0af8d0fb1af00c188831c4b5893 # v1.3.9 + uses: reviewdog/action-setup@d8a7baabd7f3e8544ee4dbde3ee41d0011c3a93f # v1.5.0 - name: Install tox run: python -m pip install tox diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml index 393ce2e73472..e8ff8ad7acbc 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@b36e8c0c10dbcfd2e05bf95f17ef8c14fd708dbf # 0.6.2 + uses: scientific-python/upload-nightly-action@5748273c71e2d8d3a61f3a11a16421c8954f9ecf # 0.6.3 with: artifacts_path: dist anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} diff --git a/.github/workflows/pr_welcome.yml b/.github/workflows/pr_welcome.yml index 3bb172ca70e7..2580b5a008a0 100644 --- a/.github/workflows/pr_welcome.yml +++ b/.github/workflows/pr_welcome.yml @@ -9,10 +9,10 @@ jobs: permissions: pull-requests: write steps: - - uses: actions/first-interaction@34f15e814fe48ac9312ccf29db4e74fa767cbab7 # v1.3.0 + - uses: actions/first-interaction@1c4688942c71f71d4f5502a26ea67c331730fa4d # v3.1.0 with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - pr-message: >+ + repo_token: ${{ secrets.GITHUB_TOKEN }} + pr_message: >+ Thank you for opening your first PR into Matplotlib! diff --git a/.github/workflows/stale-tidy.yml b/.github/workflows/stale-tidy.yml index bc50dc892155..1d0d42ce761b 100644 --- a/.github/workflows/stale-tidy.yml +++ b/.github/workflows/stale-tidy.yml @@ -9,7 +9,7 @@ jobs: if: github.repository == 'matplotlib/matplotlib' runs-on: ubuntu-latest steps: - - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} operations-per-run: 300 @@ -20,5 +20,5 @@ jobs: close-issue-label: "status: closed as inactive" days-before-issue-close: 30 ascending: true - exempt-issue-labels: "keep" + exempt-issue-labels: "keep,status: confirmed bug" exempt-pr-labels: "keep,status: orphaned PR" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index b65b44a59e88..a61a0b2e92c8 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -2,14 +2,14 @@ name: 'Label inactive PRs' on: schedule: - - cron: '30 1 * * 1,3,5' + - cron: '30 1 * * 1' jobs: stale: if: github.repository == 'matplotlib/matplotlib' runs-on: ubuntu-latest steps: - - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0 + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} operations-per-run: 20 @@ -36,3 +36,4 @@ jobs: ascending: true exempt-issue-labels: "keep" exempt-pr-labels: "keep,status: orphaned PR" + sort-by: updated diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 85ace93445b6..28e2fcacb4ae 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -60,10 +60,6 @@ jobs: extra-requirements: '-r requirements/testing/extra.txt' # https://github.com/matplotlib/matplotlib/issues/29844 pygobject-ver: '<3.52.0' - - os: ubuntu-22.04-arm - python-version: '3.12' - # https://github.com/matplotlib/matplotlib/issues/29844 - pygobject-ver: '<3.52.0' - name-suffix: "(Extra TeX packages)" os: ubuntu-22.04 python-version: '3.13' @@ -77,26 +73,35 @@ jobs: pygobject-ver: '<3.52.0' - os: ubuntu-24.04 python-version: '3.12' - - os: macos-13 # This runner is on Intel chips. - # merge numpy and pandas install in nighties test when this runner is dropped + - os: ubuntu-24.04 + python-version: '3.14' + - os: ubuntu-24.04-arm + python-version: '3.12' + - os: macos-14 # This runner is on M1 (arm64) chips. python-version: '3.11' + # https://github.com/matplotlib/matplotlib/issues/29732 + pygobject-ver: '<3.52.0' - os: macos-14 # This runner is on M1 (arm64) chips. python-version: '3.12' # https://github.com/matplotlib/matplotlib/issues/29732 pygobject-ver: '<3.52.0' - - os: macos-14 # This runner is on M1 (arm64) chips. + - os: macos-15 # This runner is on M1 (arm64) chips. python-version: '3.13' # https://github.com/matplotlib/matplotlib/issues/29732 pygobject-ver: '<3.52.0' + - os: macos-15 # This runner is on M1 (arm64) chips. + python-version: '3.14' + # https://github.com/matplotlib/matplotlib/issues/29732 + pygobject-ver: '<3.52.0' steps: - - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 with: fetch-depth: 0 persist-credentials: false - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 with: python-version: ${{ matrix.python-version }} allow-prereleases: true @@ -116,6 +121,7 @@ jobs: fonts-wqy-zenhei \ gdb \ gir1.2-gtk-3.0 \ + gir1.2-gtk-4.0 \ graphviz \ inkscape \ language-pack-de \ @@ -149,9 +155,8 @@ jobs: if [[ "${{ matrix.name-suffix }}" != '(Minimum Versions)' ]]; then sudo apt-get install -yy --no-install-recommends ffmpeg poppler-utils fi - if [[ "${{ matrix.os }}" = ubuntu-22.04 || "${{ matrix.os }}" = ubuntu-22.04-arm ]]; then + if [[ "${{ matrix.os }}" = ubuntu-22.04 ]]; then sudo apt-get install -yy --no-install-recommends \ - gir1.2-gtk-4.0 \ libgirepository1.0-dev else # ubuntu-24.04 sudo apt-get install -yy --no-install-recommends \ @@ -179,7 +184,7 @@ jobs: esac - name: Cache pip - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 if: startsWith(runner.os, 'Linux') with: path: ~/.cache/pip @@ -187,7 +192,7 @@ jobs: restore-keys: | ${{ matrix.os }}-py${{ matrix.python-version }}-pip- - name: Cache pip - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 if: startsWith(runner.os, 'macOS') with: path: ~/Library/Caches/pip @@ -195,7 +200,7 @@ jobs: restore-keys: | ${{ matrix.os }}-py${{ matrix.python-version }}-pip- - name: Cache ccache - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: | ~/.ccache @@ -203,7 +208,7 @@ jobs: restore-keys: | ${{ matrix.os }}-py${{ matrix.python-version }}-ccache- - name: Cache Matplotlib - uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 with: path: | ~/.cache/matplotlib @@ -256,7 +261,7 @@ jobs: ) # PyQt5 does not have any wheels for ARM on Linux. - if [[ "${{ matrix.os }}" != 'ubuntu-22.04-arm' ]]; then + if [[ "${{ matrix.os }}" != 'ubuntu-24.04-arm' ]]; then python -mpip install --upgrade --only-binary :all: pyqt5 && python -c 'import PyQt5.QtCore' && echo 'PyQt5 is available' || @@ -265,8 +270,8 @@ jobs: # Even though PySide2 wheels can be installed on Python 3.12+, they are broken and since PySide2 is # deprecated, they are unlikely to be fixed. For the same deprecation reason, there are no wheels # on M1 macOS, so don't bother there either. - if [[ "${{ matrix.os }}" != 'macos-14' - && "${{ matrix.python-version }}" != '3.12' && "${{ matrix.python-version }}" != '3.13' ]]; then + if [[ "${{ matrix.os }}" != 'macos-14' && "${{ matrix.python-version }}" == '3.11' + ]]; then python -mpip install --upgrade pyside2 && python -c 'import PySide2.QtCore' && echo 'PySide2 is available' || @@ -297,13 +302,7 @@ jobs: python -m pip install pytz tzdata # Must be installed for Pandas. python -m pip install \ --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \ - --upgrade --only-binary=:all: numpy - # wheels for intel osx is not always available on nightly wheels index, merge this back into - # the above install command when the OSX-13 (intel) runners are dropped. - python -m pip install \ - --index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \ - --upgrade --only-binary=:all: pandas || true - + --upgrade --only-binary=:all: numpy pandas - name: Install Matplotlib run: | @@ -344,34 +343,29 @@ jobs: - name: Cleanup non-failed image files if: failure() run: | - function remove_files() { - local extension=$1 - find ./result_images -name "*-expected*.$extension" | while read file; do - if [[ $file == *"-expected_pdf"* ]]; then - base=${file%-expected_pdf.$extension}_pdf - elif [[ $file == *"-expected_eps"* ]]; then - base=${file%-expected_eps.$extension}_eps - elif [[ $file == *"-expected_svg"* ]]; then - base=${file%-expected_svg.$extension}_svg - elif [[ $file == *"-expected_gif"* ]]; then - base=${file%-expected_gif.$extension}_gif - else - base=${file%-expected.$extension} - fi - if [[ ! -e "${base}-failed-diff.$extension" ]]; then - if [[ -e "$file" ]]; then - rm "$file" - echo "Removed $file" - fi - if [[ -e "${base}.$extension" ]]; then - rm "${base}.$extension" - echo " Removed ${base}.$extension" - fi - fi + find ./result_images -name "*-expected*.png" | while read file; do + if [[ $file == *-expected_???.png ]]; then + extension=${file: -7:3} + base=${file%*-expected_$extension.png}_$extension + else + extension="png" + base=${file%-expected.png} + fi + if [[ ! -e ${base}-failed-diff.png ]]; then + indent="" + list=($file $base.png) + if [[ $extension != "png" ]]; then + list+=(${base%_$extension}-expected.$extension ${base%_$extension}.$extension) + fi + for to_remove in "${list[@]}"; do + if [[ -e $to_remove ]]; then + rm $to_remove + echo "${indent}Removed $to_remove" + fi + indent+=" " done - } - - remove_files "png"; remove_files "svg"; remove_files "pdf"; remove_files "eps"; remove_files "gif"; + fi + done if [ "$(find ./result_images -mindepth 1 -type d)" ]; then find ./result_images/* -type d -empty -delete @@ -382,7 +376,7 @@ jobs: run: | if [[ "${{ runner.os }}" != 'macOS' ]]; then LCOV_IGNORE_ERRORS=',' # do not ignore any lcov errors by default - if [[ "${{ matrix.os }}" = ubuntu-24.04 ]]; then + if [[ "${{ matrix.os }}" = ubuntu-24.04 || "${{ matrix.os }}" = ubuntu-24.04-arm ]]; then # filter mismatch and unused-entity errors detected by lcov 2.x LCOV_IGNORE_ERRORS='mismatch,unused' fi @@ -401,12 +395,12 @@ jobs: fi - name: Upload code coverage if: ${{ !cancelled() && github.event_name != 'schedule' }} - uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 with: name: "${{ matrix.python-version }} ${{ matrix.os }} ${{ matrix.name-suffix }}" token: ${{ secrets.CODECOV_TOKEN }} - - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 if: failure() with: name: "${{ matrix.python-version }} ${{ matrix.os }} ${{ matrix.name-suffix }} result images" diff --git a/.gitignore b/.gitignore index 1d30ba69aeaa..0460152a792f 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,7 @@ pip-wheel-metadata/* .tox # build subproject files subprojects/*/ +subprojects/.* !subprojects/packagefiles/ # OS generated files # @@ -103,6 +104,10 @@ __conda_version__.txt lib/png.lib lib/z.lib +# uv files # +############ +uv.lock + # Environments # ################ .env diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 86a9a0f45440..11499188509e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ exclude: | LICENSE| lib/matplotlib/mpl-data| doc/devel/gitwash| - doc/users/prev| + doc/release/prev| doc/api/prev| lib/matplotlib/tests/data/tinypages ) diff --git a/README.md b/README.md index 7b9c99597c0d..8f9edaad2b5b 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ [![Conda](https://img.shields.io/conda/vn/conda-forge/matplotlib)](https://anaconda.org/conda-forge/matplotlib) [![Downloads](https://img.shields.io/pypi/dm/matplotlib)](https://pypi.org/project/matplotlib) [![NUMFocus](https://img.shields.io/badge/powered%20by-NumFOCUS-orange.svg?style=flat&colorA=E1523D&colorB=007D8A)](https://numfocus.org) +[![LFX Health Score](https://insights.linuxfoundation.org/api/badge/health-score?project=matplotlib)](https://insights.linuxfoundation.org/project/matplotlib) [![Discourse help forum](https://img.shields.io/badge/help_forum-discourse-blue.svg)](https://discourse.matplotlib.org) [![Gitter](https://badges.gitter.im/matplotlib/matplotlib.svg)](https://gitter.im/matplotlib/matplotlib) @@ -14,14 +15,14 @@ [![Codecov status](https://codecov.io/github/matplotlib/matplotlib/badge.svg?branch=main&service=github)](https://app.codecov.io/gh/matplotlib/matplotlib) [![EffVer Versioning](https://img.shields.io/badge/version_scheme-EffVer-0097a7)](https://jacobtomlinson.dev/effver) -![Matplotlib logotype](https://matplotlib.org/_static/logo2.svg) +![Matplotlib logotype](https://matplotlib.org/stable/_static/logo2.svg) Matplotlib is a comprehensive library for creating static, animated, and interactive visualizations in Python. Check out our [home page](https://matplotlib.org/) for more information. -![image](https://matplotlib.org/_static/readme_preview.png) +![image](https://matplotlib.org/stable/_static/readme_preview.png) Matplotlib produces publication-quality figures in a variety of hardcopy formats and interactive environments across platforms. Matplotlib can be diff --git a/ci/codespell-ignore-words.txt b/ci/codespell-ignore-words.txt index 0ebc5211b80c..e138f26e216a 100644 --- a/ci/codespell-ignore-words.txt +++ b/ci/codespell-ignore-words.txt @@ -20,3 +20,4 @@ whis wit Copin socio-economic +Ines diff --git a/ci/mypy-stubtest-allowlist.txt b/ci/mypy-stubtest-allowlist.txt index 46ec06e0a9f1..12b6feb9b2e0 100644 --- a/ci/mypy-stubtest-allowlist.txt +++ b/ci/mypy-stubtest-allowlist.txt @@ -49,3 +49,6 @@ matplotlib\.figure\.FigureBase\.get_figure # getitem method only exists for 3.10 deprecation backcompatability matplotlib\.inset\.InsetIndicator\.__getitem__ + +# only defined in stubs; not present at runtime +matplotlib\.animation\.EventSourceProtocol diff --git a/doc/_embedded_plots/figure_subplots_adjust.py b/doc/_embedded_plots/figure_subplots_adjust.py index 6f99a3febcdc..d32a029fe05d 100644 --- a/doc/_embedded_plots/figure_subplots_adjust.py +++ b/doc/_embedded_plots/figure_subplots_adjust.py @@ -7,8 +7,8 @@ overlay = fig.add_axes([0, 0, 1, 1], zorder=100) overlay.axis("off") -xycoords='figure fraction' -arrowprops=dict(arrowstyle="<->", shrinkA=0, shrinkB=0) +xycoords = 'figure fraction' +arrowprops = dict(arrowstyle="<->", shrinkA=0, shrinkB=0) for ax in axs.flat: ax.set(xticks=[], yticks=[]) diff --git a/doc/_static/switcher.json b/doc/_static/switcher.json index 62c8ed756824..36e743db21b8 100644 --- a/doc/_static/switcher.json +++ b/doc/_static/switcher.json @@ -1,7 +1,7 @@ [ { "name": "3.10 (stable)", - "version": "3.10.3", + "version": "3.10.8", "url": "https://matplotlib.org/stable/", "preferred": true }, diff --git a/doc/_static/zenodo_cache/14940554.svg b/doc/_static/zenodo_cache/14940554.svg new file mode 100644 index 000000000000..6e7d5c37bf7b --- /dev/null +++ b/doc/_static/zenodo_cache/14940554.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.14940554 + + + 10.5281/zenodo.14940554 + + + \ No newline at end of file diff --git a/doc/_static/zenodo_cache/15375714.svg b/doc/_static/zenodo_cache/15375714.svg new file mode 100644 index 000000000000..d5e403138561 --- /dev/null +++ b/doc/_static/zenodo_cache/15375714.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.15375714 + + + 10.5281/zenodo.15375714 + + + \ No newline at end of file diff --git a/doc/_static/zenodo_cache/16644850.svg b/doc/_static/zenodo_cache/16644850.svg new file mode 100644 index 000000000000..89910032da4e --- /dev/null +++ b/doc/_static/zenodo_cache/16644850.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.16644850 + + + 10.5281/zenodo.16644850 + + + \ No newline at end of file diff --git a/doc/_static/zenodo_cache/16999430.svg b/doc/_static/zenodo_cache/16999430.svg new file mode 100644 index 000000000000..44c448643e91 --- /dev/null +++ b/doc/_static/zenodo_cache/16999430.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.16999430 + + + 10.5281/zenodo.16999430 + + + \ No newline at end of file diff --git a/doc/_static/zenodo_cache/17298696.svg b/doc/_static/zenodo_cache/17298696.svg new file mode 100644 index 000000000000..9aa8d7c94349 --- /dev/null +++ b/doc/_static/zenodo_cache/17298696.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.17298696 + + + 10.5281/zenodo.17298696 + + + \ No newline at end of file diff --git a/doc/api/artist_api.rst b/doc/api/artist_api.rst index 0ca3fb364c41..f256d2b7164e 100644 --- a/doc/api/artist_api.rst +++ b/doc/api/artist_api.rst @@ -122,6 +122,7 @@ Figure and Axes Artist.set_figure Artist.get_figure + Artist.figure Children -------- diff --git a/doc/api/axes_api.rst b/doc/api/axes_api.rst index b742ce9b7a55..1868794dd921 100644 --- a/doc/api/axes_api.rst +++ b/doc/api/axes_api.rst @@ -73,6 +73,7 @@ Basic Axes.eventplot Axes.pie + Axes.pie_label Axes.stackplot @@ -627,6 +628,8 @@ Other Axes.get_transformed_clip_path_and_affine Axes.has_data Axes.set + Axes.get_figure + Axes.figure Axes.remove .. autoclass:: matplotlib.axes.Axes.ArtistList diff --git a/doc/api/colors_api.rst b/doc/api/colors_api.rst index 49a42c8f9601..18e7c43932a9 100644 --- a/doc/api/colors_api.rst +++ b/doc/api/colors_api.rst @@ -32,6 +32,7 @@ Color norms PowerNorm SymLogNorm TwoSlopeNorm + MultiNorm Univariate Colormaps -------------------- diff --git a/doc/api/matplotlib_configuration_api.rst b/doc/api/matplotlib_configuration_api.rst index d5dc60c80613..b2fafc08e5fc 100644 --- a/doc/api/matplotlib_configuration_api.rst +++ b/doc/api/matplotlib_configuration_api.rst @@ -24,8 +24,24 @@ Default values and styling ========================== .. py:data:: rcParams + :type: RcParams + + The global configuration settings for Matplotlib. + + This is a dictionary-like variable that stores the current configuration + settings. Many of the values control styling, but others control + various aspects of Matplotlib's behavior. + + See :doc:`/users/explain/configuration` for a full list of config + parameters. + + See :ref:`customizing` for usage information. + + Notes + ----- + This object is also available as ``plt.rcParams`` via the + `matplotlib.pyplot` module (which by convention is imported as ``plt``). - An instance of `RcParams` for handling default Matplotlib values. .. autoclass:: RcParams :no-members: diff --git a/doc/api/next_api_changes/behavior/28437-CH.rst b/doc/api/next_api_changes/behavior/28437-CH.rst index 6121dfec8163..bb303bbe9d3b 100644 --- a/doc/api/next_api_changes/behavior/28437-CH.rst +++ b/doc/api/next_api_changes/behavior/28437-CH.rst @@ -2,7 +2,7 @@ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When passing and array to ``imshow(..., alpha=...)``, the parameter was silently ignored -if the image data was a RGB or RBGA image or if :rc:`interpolation_state` +if the image data was an RGB or RBGA image or if :rc:`image.interpolation_stage` resolved to "rbga". This is now fixed, and the alpha array overwrites any previous transparency information. diff --git a/doc/api/next_api_changes/behavior/29958-TH.rst b/doc/api/next_api_changes/behavior/29958-TH.rst new file mode 100644 index 000000000000..cacaf2bac612 --- /dev/null +++ b/doc/api/next_api_changes/behavior/29958-TH.rst @@ -0,0 +1,8 @@ +``Axes.add_collection(..., autolim=True)`` updates view limits +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``Axes.add_collection(..., autolim=True)`` has so far only updated the data limits. +Users needed to additionally call `.Axes.autoscale_view` to update the view limits. +View limits are now updated as well if ``autolim=True``, using a lazy internal +update mechanism, so that the costs only apply once also if you add multiple +collections. diff --git a/doc/api/next_api_changes/behavior/30272-ES.rst b/doc/api/next_api_changes/behavior/30272-ES.rst new file mode 100644 index 000000000000..5a03f9bc7972 --- /dev/null +++ b/doc/api/next_api_changes/behavior/30272-ES.rst @@ -0,0 +1,2 @@ +``font_manager.findfont`` logs if selected font weight does not match requested +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/api/next_api_changes/behavior/30532-TH.rst b/doc/api/next_api_changes/behavior/30532-TH.rst new file mode 100644 index 000000000000..3d368c566039 --- /dev/null +++ b/doc/api/next_api_changes/behavior/30532-TH.rst @@ -0,0 +1,4 @@ +Default name of ``ListedColormap`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The default name of `.ListedColormap` has changed from "from_list" to "unnamed". diff --git a/doc/api/next_api_changes/behavior/30634-AL.rst b/doc/api/next_api_changes/behavior/30634-AL.rst new file mode 100644 index 000000000000..585de1ea14eb --- /dev/null +++ b/doc/api/next_api_changes/behavior/30634-AL.rst @@ -0,0 +1,6 @@ +hist2d no longer forces axes limits +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Previously, `.Axes.hist2d` would force the axes x and y limits to the extents +of the histogrammed data, ignoring any other artists. `.Axes.hist2d` now +behaves similarly to `.Axes.imshow`: axes limits are updated to fit the data, +but autoscaling is not otherwise disabled. diff --git a/doc/api/next_api_changes/behavior/30824-AYS.rst b/doc/api/next_api_changes/behavior/30824-AYS.rst new file mode 100644 index 000000000000..a190bd537126 --- /dev/null +++ b/doc/api/next_api_changes/behavior/30824-AYS.rst @@ -0,0 +1,6 @@ +Bivariate colormaps now fully span the intended range of colors +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Bivariate colormaps generated by ``SegmentedBivarColormap`` (e.g., ``BiOrangeBlue``) +from a set of input colors now fully span that range of colors. There had been a bug +with the numerical interpolation such that the colormap did not actually include the +first or last colors. diff --git a/doc/api/next_api_changes/deprecations/29358-TH.rst b/doc/api/next_api_changes/deprecations/29358-TH.rst new file mode 100644 index 000000000000..1b7a50456afc --- /dev/null +++ b/doc/api/next_api_changes/deprecations/29358-TH.rst @@ -0,0 +1,17 @@ +3rd party scales do not need to have an *axis* parameter anymore +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since matplotlib 3.1 `PR 12831 `_ +scale objects should be reusable and therefore independent of any particular Axis. +Therefore, the use of the *axis* parameter in the ``__init__`` had been discouraged. +However, having that parameter in the signature was still necessary for API +backwards-compatibility. This is no longer the case. + +`.register_scale` now accepts scale classes with or without this parameter. + +The *axis* parameter is pending-deprecated. It will be deprecated in matplotlib 3.13, +and removed in matplotlib 3.15. + +3rd-party scales are recommended to remove the *axis* parameter now if they can +afford to restrict compatibility to matplotlib >= 3.11 already. Otherwise, they may +keep the *axis* parameter and remove it in time for matplotlib 3.13. diff --git a/doc/api/next_api_changes/deprecations/29993-AL.rst b/doc/api/next_api_changes/deprecations/29993-AL.rst new file mode 100644 index 000000000000..9104fd669325 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/29993-AL.rst @@ -0,0 +1,4 @@ +``testing.widgets.mock_event`` and ``testing.widgets.do_event`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... are deprecated. Directly construct Event objects (typically `.MouseEvent` +or `.KeyEvent`) and pass them to ``canvas.callbacks.process()`` instead. diff --git a/doc/api/next_api_changes/deprecations/30349-AL.rst b/doc/api/next_api_changes/deprecations/30349-AL.rst new file mode 100644 index 000000000000..78e26f41889f --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30349-AL.rst @@ -0,0 +1,3 @@ +``Axes.set_navigate_mode`` is deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... with no replacement. diff --git a/doc/api/next_api_changes/deprecations/30364-AS.rst b/doc/api/next_api_changes/deprecations/30364-AS.rst new file mode 100644 index 000000000000..4f5493b8b706 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30364-AS.rst @@ -0,0 +1,4 @@ +Parameters ``Axes3D.set_aspect(..., anchor=..., share=...)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The parameters *anchor* and *share* of `.Axes3D.set_aspect` are deprecated. +They had no effect on 3D axes and will be removed in a future version. diff --git a/doc/api/next_api_changes/deprecations/30368-AL.rst b/doc/api/next_api_changes/deprecations/30368-AL.rst new file mode 100644 index 000000000000..efd3c8360ef3 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30368-AL.rst @@ -0,0 +1,3 @@ +``GridFinder.get_grid_info`` now takes a single bbox as parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Passing ``x1, y1, x2, y2`` as separate parameters is deprecated. diff --git a/doc/api/next_api_changes/deprecations/30469-AL.rst b/doc/api/next_api_changes/deprecations/30469-AL.rst new file mode 100644 index 000000000000..ef3f042843c2 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30469-AL.rst @@ -0,0 +1,4 @@ +The *axes* parameter of ``RadialLocator`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... is deprecated. `~.polar.RadialLocator` now fetches the relevant information +from the Axis' parent Axes. diff --git a/doc/api/next_api_changes/deprecations/30531-TH.rst b/doc/api/next_api_changes/deprecations/30531-TH.rst new file mode 100644 index 000000000000..19d51fd2fb6c --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30531-TH.rst @@ -0,0 +1,16 @@ +In-place modifications of colormaps +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Colormaps are planned to become immutable in the long term. + +As a first step, in-place modifications of colormaps are now pending-deprecated. +This affects the following methods of `.Colormap`: + +- `.Colormap.set_bad` - use ``cmap.with_extremes(bad=...)`` instead +- `.Colormap.set_under` - use ``cmap.with_extremes(under=...)`` instead +- `.Colormap.set_over` - use ``cmap.with_extremes(over=...)`` instead +- `.Colormap.set_extremes` - use ``cmap.with_extremes(...)`` instead + +Use the respective `.Colormap.with_extremes` and appropriate keyword arguments +instead which returns a copy of the colormap (available since matplotlib 3.4). +Alternatively, if you create the colormap yourself, you can also pass the +respective arguments to the constructor (available since matplotlib 3.11). diff --git a/doc/api/next_api_changes/deprecations/30737-TH.rst b/doc/api/next_api_changes/deprecations/30737-TH.rst new file mode 100644 index 000000000000..8feef85912b4 --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30737-TH.rst @@ -0,0 +1,8 @@ +The *canvas* parameter to ``MultiCursor`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... is deprecated. It has been unused for a while already. + +Please remove the parameter and change the call from +``MultiCursor(canvas, axes)`` to ``MultiCursor(axes)``. Both calls are +valid throughout the deprecation period. diff --git a/doc/api/next_api_changes/deprecations/30889-TH.rst b/doc/api/next_api_changes/deprecations/30889-TH.rst new file mode 100644 index 000000000000..d221ae30d4fb --- /dev/null +++ b/doc/api/next_api_changes/deprecations/30889-TH.rst @@ -0,0 +1,10 @@ +Transforms helper functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following functions in the `.transforms` module are deprecated, +because they are considerer internal functionality and should not be used +by end users: + +- ``matplotlib.transforms.nonsingular`` +- ``matplotlib.transforms.interval_contains`` +- ``matplotlib.transforms.interval_contains_open`` diff --git a/doc/api/prev_api_changes/api_changes_3.10.1.rst b/doc/api/prev_api_changes/api_changes_3.10.1.rst index 26d43ddf8b17..71a2f71efc94 100644 --- a/doc/api/prev_api_changes/api_changes_3.10.1.rst +++ b/doc/api/prev_api_changes/api_changes_3.10.1.rst @@ -8,7 +8,7 @@ Behaviour ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When passing and array to ``imshow(..., alpha=...)``, the parameter was silently ignored -if the image data was a RGB or RBGA image or if :rc:`interpolation_state` +if the image data was an RGB or RBGA image or if :rc:`image.interpolation_stage` resolved to "rbga". This is now fixed, and the alpha array overwrites any previous transparency information. diff --git a/doc/api/prev_api_changes/api_changes_3.10.7.rst b/doc/api/prev_api_changes/api_changes_3.10.7.rst new file mode 100644 index 000000000000..a60061e86277 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.10.7.rst @@ -0,0 +1,10 @@ +API Changes for 3.10.7 +====================== + +Development +----------- + +New minimum version of pyparsing +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The minimum required version of ``pyparsing`` has been updated from 2.3.1 to 3.0.0. diff --git a/doc/api/pyplot_summary.rst b/doc/api/pyplot_summary.rst index c4a860fd2590..97d9c576cc86 100644 --- a/doc/api/pyplot_summary.rst +++ b/doc/api/pyplot_summary.rst @@ -64,6 +64,7 @@ Basic stem eventplot pie + pie_label stackplot broken_barh vlines diff --git a/doc/api/toolkits/mplot3d/faq.rst b/doc/api/toolkits/mplot3d/faq.rst index e9ba804648e0..20fe81e574fe 100644 --- a/doc/api/toolkits/mplot3d/faq.rst +++ b/doc/api/toolkits/mplot3d/faq.rst @@ -6,8 +6,7 @@ mplot3d FAQ How is mplot3d different from Mayavi? ===================================== -`Mayavi `_ -is a very powerful and featureful 3D graphing library. For advanced +Mayavi_ is a very powerful and featureful 3D graphing library. For advanced 3D scenes and excellent rendering capabilities, it is highly recommended to use Mayavi. @@ -37,8 +36,7 @@ rendered properly in matplotlib's 2D rendering engine. This problem will likely not be solved until OpenGL support is added to all of the backends (patches are greatly welcomed). Until then, if you need complex -3D scenes, we recommend using -`MayaVi `_. +3D scenes, we recommend using Mayavi_. I don't like how the 3D plot is laid out, how do I change that? @@ -49,3 +47,5 @@ Work is being done to eliminate this issue. For matplotlib v1.1.0, there is a semi-official manner to modify these parameters. See the note in the :mod:`.mplot3d.axis3d` section of the mplot3d API documentation for more information. + +.. _Mayavi: https://docs.enthought.com/mayavi/mayavi/ diff --git a/doc/api/toolkits/mplot3d/view_angles.rst b/doc/api/toolkits/mplot3d/view_angles.rst index 75b24ba9c7b0..e4200cd2d0e4 100644 --- a/doc/api/toolkits/mplot3d/view_angles.rst +++ b/doc/api/toolkits/mplot3d/view_angles.rst @@ -11,8 +11,7 @@ The position of the viewport "camera" in a 3D plot is defined by three angles: *elevation*, *azimuth*, and *roll*. From the resulting position, it always points towards the center of the plot box volume. The angle direction is a common convention, and is shared with -`PyVista `_ and -`MATLAB `_. +`PyVista `_ and MATLAB_. Note that a positive roll angle rotates the viewing plane clockwise, so the 3d axes will appear to rotate counter-clockwise. @@ -51,8 +50,7 @@ can be specified by setting :rc:`axes3d.mouserotationstyle`, see :doc:`/users/explain/customizing`. Prior to v3.10, the 2D mouse position corresponded directly -to azimuth and elevation; this is also how it is done -in `MATLAB `_. +to azimuth and elevation; this is also how it is done in MATLAB_. To keep it this way, set ``mouserotationstyle: azel``. This approach works fine for spherical coordinate plots, where the *z* axis is special; however, it leads to a kind of 'gimbal lock' when looking down the *z* axis: @@ -131,7 +129,7 @@ Henriksen et al. [Henriksen2002]_ provide an overview. In summary: You can try out one of the various mouse rotation styles using: -.. code:: +.. code-block:: python import matplotlib as mpl mpl.rcParams['axes3d.mouserotationstyle'] = 'trackball' # 'azel', 'trackball', 'sphere', or 'arcball' @@ -188,6 +186,7 @@ the arcball to the border occurs at 45°, set the border width to The border is a circular arc, wrapped around the arcball sphere cylindrically (like a doughnut), joined smoothly to the sphere, much like Bell's hyperbola. +.. _MATLAB: https://www.mathworks.com/help/matlab/ref/view.html .. [Shoemake1992] Ken Shoemake, "ARCBALL: A user interface for specifying three-dimensional rotation using a mouse", in Proceedings of Graphics diff --git a/doc/api/widgets_api.rst b/doc/api/widgets_api.rst index 014361e70377..739b0f8931e0 100644 --- a/doc/api/widgets_api.rst +++ b/doc/api/widgets_api.rst @@ -2,11 +2,53 @@ ``matplotlib.widgets`` ********************** -.. inheritance-diagram:: matplotlib.widgets +.. currentmodule:: matplotlib.widgets + +.. automodule:: matplotlib.widgets + :no-members: + :no-undoc-members: + + +Widget classes +============== + +.. inheritance-diagram:: matplotlib.widgets.Widget :parts: 1 + :private-bases: + :include-subclasses: +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: -.. automodule:: matplotlib.widgets - :members: - :undoc-members: - :show-inheritance: + Widget + AxesWidget + Cursor + MultiCursor + Button + CheckButtons + RadioButtons + SliderBase + Slider + RangeSlider + TextBox + _SelectorWidget + RectangleSelector + EllipseSelector + Lasso + LassoSelector + PolygonSelector + SpanSelector + SubplotTool + +Helper classes +============== + +.. autosummary:: + :toctree: _as_gen + :nosignatures: + + LockDraw + ToolHandles + ToolLineHandles diff --git a/doc/conf.py b/doc/conf.py index 199249fdd437..806561f0bfc0 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -57,7 +57,7 @@ def _parse_skip_subdirs_file(): can make partial builds very fast. """ default_skip_subdirs = [ - 'users/prev_whats_new/*', 'users/explain/*', 'api/*', 'gallery/*', + 'release/prev_whats_new/*', 'users/explain/*', 'api/*', 'gallery/*', 'tutorials/*', 'plot_types/*', 'devel/*'] try: with open(".mpl_skip_subdirs.yaml", 'r') as fin: @@ -208,8 +208,6 @@ def _check_dependencies(): else: sg_matplotlib_animations = True -# The following import is only necessary to monkey patch the signature later on -from sphinx_gallery import gen_rst # Prevent plt.show() from emitting a non-GUI backend warning. warnings.filterwarnings('ignore', category=UserWarning, @@ -307,7 +305,7 @@ def autodoc_process_bases(app, name, obj, options, bases): 'reference_url': {'matplotlib': None, 'mpl_toolkits': None}, 'prefer_full_module': {r'mpl_toolkits\.'}, 'remove_config_comments': True, - 'reset_modules': ('matplotlib', clear_basic_units), + 'reset_modules': ('matplotlib', clear_basic_units, 'sphinxext.util.patch_header'), 'subsection_order': gallery_order_sectionorder, 'thumbnail_size': (320, 224), 'within_subsection_order': gallery_order_subsectionorder, @@ -355,31 +353,6 @@ def gallery_image_warning_filter(record): mathmpl_fontsize = 11.0 mathmpl_srcset = ['2x'] -# Monkey-patching gallery header to include search keywords -gen_rst.EXAMPLE_HEADER = """ -.. DO NOT EDIT. -.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. -.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: -.. "{0}" -.. LINE NUMBERS ARE GIVEN BELOW. - -.. only:: html - - .. meta:: - :keywords: codex - - .. note:: - :class: sphx-glr-download-link-note - - :ref:`Go to the end ` - to download the full example code.{2} - -.. rst-class:: sphx-glr-example-title - -.. _sphx_glr_{1}: - -""" - # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -595,7 +568,7 @@ def js_tag_with_cache_busting(js): # no sidebar for release notes, because that page is only a collection of links # to sub-pages. The sidebar would repeat all the titles of the sub-pages and # thus basically repeat all the content of the page. - "users/release_notes": ["empty_sidebar.html"], + "release/release_notes": ["empty_sidebar.html"], # '**': ['localtoc.html', 'pagesource.html'] } diff --git a/doc/devel/MEP/MEP10.rst b/doc/devel/MEP/MEP10.rst index 9e9650587f55..2b39959eaca7 100644 --- a/doc/devel/MEP/MEP10.rst +++ b/doc/devel/MEP/MEP10.rst @@ -44,8 +44,7 @@ these new features. Numpy docstring format ---------------------- -`Numpy docstring format -`_: +`Numpy docstring format `_: This format divides the docstring into clear sections, each having different parsing rules that make the docstring easy to read both as raw text and as HTML. We could consider alternatives, or invent our diff --git a/doc/devel/MEP/MEP11.rst b/doc/devel/MEP/MEP11.rst index aee44ae9a0e4..03bc3013b3e3 100644 --- a/doc/devel/MEP/MEP11.rst +++ b/doc/devel/MEP/MEP11.rst @@ -130,7 +130,7 @@ ordered from best/hardest to worst/easiest): 1. The distutils wininst installer allows a post-install script to run. It might be possible to get this script to run pip_ to install the other dependencies. (See `this thread - `_ + `_ for someone who has trod that ground before). 2. Continue to ship dateutil_, pytz_, six_ and pyparsing_ in @@ -177,4 +177,4 @@ out of the box. .. _pytz: https://pypi.org/project/pytz/ .. _setuptools: https://pypi.org/project/setuptools/ .. _six: https://pypi.org/project/six/ -.. _easy_install: https://setuptools.readthedocs.io/en/latest/easy_install.html +.. _easy_install: https://setuptools.pypa.io/en/latest/deprecated/easy_install.html diff --git a/doc/devel/MEP/MEP14.rst b/doc/devel/MEP/MEP14.rst index 2c696adf8a58..d79d3c2d3115 100644 --- a/doc/devel/MEP/MEP14.rst +++ b/doc/devel/MEP/MEP14.rst @@ -78,7 +78,7 @@ number of other projects: - `Microsoft DirectWrite`_ - `Apple Core Text`_ -.. _pango: https://pango.gnome.org +.. _pango: https://github.com/GNOME/pango .. _harfbuzz: https://github.com/harfbuzz/harfbuzz .. _QtTextLayout: https://doc.qt.io/archives/qt-4.8/qtextlayout.html .. _Microsoft DirectWrite: https://docs.microsoft.com/en-ca/windows/win32/directwrite/introducing-directwrite diff --git a/doc/devel/api_changes.rst b/doc/devel/api_changes.rst index 19bc530abf6b..5fed9f683a48 100644 --- a/doc/devel/api_changes.rst +++ b/doc/devel/api_changes.rst @@ -220,7 +220,7 @@ folder: +-------------------+-----------------------------+----------------------------------------------+ | | versioning directive | announcement folder | +===================+=============================+==============================================+ -| new feature | ``.. versionadded:: 3.N`` | :file:`doc/users/next_whats_new/` | +| new feature | ``.. versionadded:: 3.N`` | :file:`doc/release/next_whats_new/` | +-------------------+-----------------------------+----------------------------------------------+ | API change | ``.. versionchanged:: 3.N`` | :file:`doc/api/next_api_changes/[kind]` | +-------------------+-----------------------------+----------------------------------------------+ @@ -306,7 +306,7 @@ API change notes What's new notes """""""""""""""" -.. include:: ../users/next_whats_new/README.rst +.. include:: ../release/next_whats_new/README.rst :start-after: whats-new-guide-start :end-before: whats-new-guide-end diff --git a/doc/devel/coding_guide.rst b/doc/devel/coding_guide.rst index 2b156cedca05..fe7769909368 100644 --- a/doc/devel/coding_guide.rst +++ b/doc/devel/coding_guide.rst @@ -215,7 +215,7 @@ If an end-user of Matplotlib sets up `logging` to display at levels more verbose than ``logging.WARNING`` in their code with the Matplotlib-provided helper:: - plt.set_loglevel("debug") + plt.set_loglevel("DEBUG") or manually with :: diff --git a/doc/devel/communication_guide.rst b/doc/devel/communication_guide.rst index e44d9368da93..c90d1d93b99d 100644 --- a/doc/devel/communication_guide.rst +++ b/doc/devel/communication_guide.rst @@ -215,7 +215,7 @@ On social media, Matplotlib: * Highlights various parts of the library, especially the more obscure bits and bobbles. * Acknowledges that it is a sometimes frustrating tangle of bits & bobbles that - can confuse even the folks who work on it & signal boosts their confuzzlment. + can confuse even the folks who work on it & signal boosts their confuzzlement. Behavior diff --git a/doc/devel/contribute.rst b/doc/devel/contribute.rst index 558e19790d82..bd4fe8e64c52 100644 --- a/doc/devel/contribute.rst +++ b/doc/devel/contribute.rst @@ -29,7 +29,8 @@ Ways to contribute * **You are a Matplotlib user, and you see a bug, a potential improvement, or something that annoys you, and you can fix it.** - You can search our issue tracker for an existing issue that describes your problem or + You can search our `issue tracker `__ + for an existing issue that describes your problem or open a new issue to inform us of the problem you observed and discuss the best approach to fix it. If your contributions would not be captured on GitHub (social media, communication, educational content), you can also reach out to us on gitter_, @@ -42,14 +43,11 @@ Ways to contribute Awesome — you have a focus on a specific application and domain and can start there. In this case, maintainers can help you figure out the best - implementation; open an issue or pull request with a starting point, and we'll - be happy to discuss technical approaches. + implementation; `open an issue `__ + in our issue tracker, and we'll be happy to discuss technical approaches. - If you prefer, you can use the `GitHub functionality for "draft" pull requests - `__ - and request early feedback on whatever you are working on, but you should be - aware that maintainers may not review your contribution unless it has the - "Ready to review" state on GitHub. + If you can implement the solution yourself, even better! Consider contributing + the change as a :ref:`pull request ` right away. * **You are new to Matplotlib, both as a user and contributor, and want to start contributing but have yet to develop a particular interest.** @@ -190,13 +188,20 @@ If you have developed an extension to Matplotlib, please consider adding it to o Restrictions on Generative AI Usage =================================== -We expect authentic engagement in our community. Be wary of posting output -from Large Language Models or similar generative AI as comments on GitHub or -our discourse server, as such comments tend to be formulaic and low content. -If you use generative AI tools as an aid in developing code or documentation -changes, ensure that you fully understand the proposed changes and can explain -why they are the correct approach and an improvement to the current state. +We expect authentic engagement in our community. +- Do not post output from Large Language Models or similar generative AI as + comments on GitHub or our discourse server, as such comments tend to be + formulaic and low content. +- If you use generative AI tools as an aid in developing code or documentation + changes, ensure that you fully understand the proposed changes and can + explain why they are the correct approach. + +Make sure you have added value based on your personal competency to your +contributions. Just taking some input, feeding it to an AI and posting the +result is not of value to the project. To preserve precious core developer +capacity, we reserve the right to rigorously reject seemingly AI generated +low-value contributions. .. _new_contributors: @@ -240,11 +245,11 @@ process works, technical questions about the code, what makes for good documentation or a blog post, how to get involved in community work, or get a "pre-review" on your PR. -To join, please go to our public community_ channel, and ask to be added to +To join, please go to our public `community gitter`_ channel, and ask to be added to ``#incubator``. One of our core developers will see your message and will add you. .. _gitter: https://gitter.im/matplotlib/matplotlib -.. _community: https://gitter.im/matplotlib/community +.. _community gitter: https://gitter.im/matplotlib/community .. _good_first_issues: @@ -287,7 +292,7 @@ guide you through each step: 4. Check existing pull requests (e.g., :ghpull:`28476`) and filter by the issue number to make sure the issue is not in progress: * If the issue has a pull request (is in progress), tag the user working on the issue, and ask to collaborate (optional). - * If a pull request does not exist, create a `draft pull request `_ and follow the `pull request guidelines `_. + * If there is no pull request, :ref:`create a new pull request `. 5. Please familiarize yourself with the pull request template (see below), and ensure you understand/are able to complete the template when you open your pull request. Additional information can be found in the `pull request guidelines `_. @@ -308,10 +313,7 @@ active contributors, many of whom felt just like you when they started out and are happy to welcome you and support you as you get to know how we work, and where things are. You can reach out on any of our :ref:`communication-channels`. For development questions we recommend reaching out on our development gitter_ -chat room and for community questions reach out at community_. - -.. _gitter: https://gitter.im/matplotlib/matplotlib -.. _community: https://gitter.im/matplotlib/community +chat room and for community questions reach out at `community gitter`_. .. _managing_issues_prs: @@ -335,7 +337,7 @@ Start a pull request The preferred way to contribute to Matplotlib is to fork the `main repository `__ on GitHub, -then submit a "pull request" (PR). To work on a a pull request: +then submit a "pull request" (PR). To work on a pull request: #. **First** set up a development environment, either by cloning a copy of the Matplotlib repository to your own computer or by using Github codespaces, by diff --git a/doc/devel/development_setup.rst b/doc/devel/development_setup.rst index 45b95e48e7ff..4e452fb3bfe7 100644 --- a/doc/devel/development_setup.rst +++ b/doc/devel/development_setup.rst @@ -38,7 +38,7 @@ Set up development environment ============================== You can either work locally on your machine, or online in -`GitHub Codespaces `_, a cloud-based in-browser development +`GitHub Codespaces`_, a cloud-based in-browser development environment. @@ -119,8 +119,8 @@ code, as described in :ref:`development-workflow`. * `GitHub-Contributing to a Project `_ * `GitHub Skills `_ - * :ref:`using-git` - * :ref:`git-resources` + * :external+scipy:ref:`using-git` + * :external+scipy:ref:`git-resources` * `Installing git `_ * `Managing remote repositories `_ @@ -219,7 +219,7 @@ need to be installed when working in codespaces. Create GitHub Codespace :octicon:`codespaces` --------------------------------------------- -`GitHub Codespaces `_ is a cloud-based +`GitHub Codespaces`_ is a cloud-based in-browser development environment that comes with the appropriate setup to contribute to Matplotlib. @@ -260,7 +260,7 @@ Use the "Extensions" icon in the activity bar to install the "Live Server" extension. Locate the ``doc/build/html`` folder in the Explorer, right click the file you want to open and select "Open with Live Server." -.. _`github-codespaces`: https://docs.github.com/codespaces +.. _Github Codespaces: https://docs.github.com/codespaces .. _development-install: diff --git a/doc/devel/development_workflow.rst b/doc/devel/development_workflow.rst index 16766278f658..c0300acf1f7f 100644 --- a/doc/devel/development_workflow.rst +++ b/doc/devel/development_workflow.rst @@ -179,9 +179,9 @@ Enter a title for the set of changes with some explanation of what you've done. Mention anything you'd like particular attention for - such as a complicated change or some code you are not happy with. -If you don't think your request is ready to be merged, just say so in your pull -request message and use the "Draft PR" feature of GitHub. This is a good way of -getting some preliminary code review. +If you don't think your request is ready to be merged, make a +:ref:`draft pull request ` and state what aspects you want to have +feedback on. This is a good way of getting some preliminary code review. For more guidance on the mechanics of making a pull request, see GitHub's `pull request tutorial `_. diff --git a/doc/devel/index.rst b/doc/devel/index.rst index 7591359ec811..fdeb08d3b202 100644 --- a/doc/devel/index.rst +++ b/doc/devel/index.rst @@ -189,8 +189,13 @@ and managing a development environment and workflow: Policies and guidelines ======================= -These policies and guidelines help us maintain consistency in the various types -of maintenance work. If you are writing code or documentation, following these policies +.. admonition:: AI Usage + + AI may be used responsibly as a supportive tool, but we expect authentic + contributions. For guidance, see our :ref:`AI policy `. + +These policies and guidelines help us maintain consistency in the various types of +maintenance work. If you are writing code or documentation, following these policies helps maintainers more easily review your work. If you are helping triage, community manage, or release manage, these guidelines describe how our current process works. diff --git a/doc/devel/license.rst b/doc/devel/license.rst index 7596f2f92348..ebdfd494f832 100644 --- a/doc/devel/license.rst +++ b/doc/devel/license.rst @@ -10,7 +10,7 @@ another project make sure it has a PSF, BSD, MIT or compatible license licenses). If it doesn't, you may consider contacting the author and asking them to relicense it. GPL and LGPL code are not acceptable in the main code base, though we are considering an alternative way of -distributing L/GPL code through an separate channel, possibly a +distributing L/GPL code through a separate channel, possibly a toolkit. If you include code, make sure you include a copy of that code's license in the license directory if the code's license requires you to distribute the license with it. Non-BSD compatible licenses diff --git a/doc/devel/pr_guide.rst b/doc/devel/pr_guide.rst index a02b52ad5a38..e7e3ceba8f95 100644 --- a/doc/devel/pr_guide.rst +++ b/doc/devel/pr_guide.rst @@ -12,7 +12,7 @@ We value contributions from people with all levels of experience. In particular, if this is your first PR not everything has to be perfect. We'll guide you through the PR process. Nevertheless, please try to follow our guidelines as well as you can to help make the PR process quick and smooth. If your pull request is -incomplete or a work-in-progress, please mark it as a `draft pull requests `_ +incomplete or a work-in-progress, please mark it as a :ref:`draft pull request ` on GitHub and specify what feedback from the developers would be helpful. Please be patient with reviewers. We try our best to respond quickly, but we have @@ -109,15 +109,32 @@ Workflow * The PR should :ref:`target the main branch `. * Tag with descriptive :ref:`labels `. * Set the :ref:`milestone `. -* Keep an eye on the :ref:`number of commits `. +* :ref:`Review ` the contents. * Approve if all of the above topics are handled. -* :ref:`Merge ` if a sufficient number of approvals is reached. +* Keep an eye on the :ref:`number of commits `. +* :ref:`Merge ` if a :ref:`sufficient number of approvals ` is reached. .. _pr-guidelines-details: Detailed guidelines =================== +.. _draft-pr: + +Draft PRs +--------- + +Authors may create a `draft PR`_ (or change to draft status later) if the code +is not yet ready for a regular full review. Typical use cases are posting code +as a basis for discussion or signalling that you intend to rework the code as +a result of feedback. Authors should clearly communicate why the PR has draft +status and what needs to be done to make it ready for review. In particular, +they should explicitly ask for targeted feedback if needed. By default, +reviewers will not look at the code of a draft PR and only respond to specific +questions by the author. + +.. _draft PR: https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests#draft-pull-requests + .. _pr-documentation: Documentation @@ -174,10 +191,27 @@ All Pull Requests should target the main branch. The milestone tag triggers an :ref:`automatic backport ` for milestones which have a corresponding branch. -.. _pr-merging: +.. _pr-review: -Merging -------- +Review +------ + +* Do not let perfect be the enemy of the good, particularly for + documentation or example PRs. If you find yourself making many + small suggestions, either open a PR against the original branch, + push changes to the contributor branch, or merge the PR and then + open a new PR against upstream. + +* If you push to a contributor branch leave a comment explaining what + you did, ex "I took the liberty of pushing a small clean-up PR to + your branch, thanks for your work.". If you are going to make + substantial changes to the code or intent of the PR please check + with the contributor first. + +.. _pr-approval: + +Approval +-------- As a guiding principle, we require two `approvals`_ from core developers (those with commit rights) before merging a pull request. This two-pairs-of-eyes strategy shall ensure a consistent project direction and prevent accidental @@ -213,11 +247,6 @@ Some explicit rules following from this: A core dev should only champion one PR at a time and we should try to keep the flow of championed PRs reasonable. -After giving the last required approval, the author of the approval should -merge the PR. PR authors should not self-merge except for when another reviewer -explicitly allows it (e.g., "Approve modulo CI passing, may self merge when -green", or "Take or leave the comments. You may self merge".). - .. _pr-automated-tests: Automated tests @@ -225,6 +254,15 @@ Automated tests Before being merged, a PR should pass the :ref:`automated-tests`. If you are unsure why a test is failing, ask on the PR or in our :ref:`communication-channels` +.. _pr-merging: + +Merging +------- +After giving the last required :ref:`approval `, the author of the +approval should merge the PR. PR authors should not self-merge except for when +another reviewer explicitly allows it (e.g., "Approve modulo CI passing, may +self-merge when green", or "Take or leave the comments. You may self merge".). + .. _pr-squashing: Number of commits and squashing @@ -236,19 +274,6 @@ Number of commits and squashing about it is to eliminate binary files (ex multiple test image re-generations) and to remove upstream merges. -* Do not let perfect be the enemy of the good, particularly for - documentation or example PRs. If you find yourself making many - small suggestions, either open a PR against the original branch, - push changes to the contributor branch, or merge the PR and then - open a new PR against upstream. - -* If you push to a contributor branch leave a comment explaining what - you did, ex "I took the liberty of pushing a small clean-up PR to - your branch, thanks for your work.". If you are going to make - substantial changes to the code or intent of the PR please check - with the contributor first. - - .. _branches_and_backports: Branches and backports diff --git a/doc/devel/release_guide.rst b/doc/devel/release_guide.rst index 6c45bfa56c64..886b60240415 100644 --- a/doc/devel/release_guide.rst +++ b/doc/devel/release_guide.rst @@ -83,7 +83,11 @@ Micro versions should instead read:: Check all active milestones for consistency. Older milestones should also backport to higher meso versions (e.g. ``v3.6.3`` and ``v3.6-doc`` should backport to both ``v3.6.x`` and ``v3.7.x`` once the ``v3.7.x`` branch exists and while PR backports are -still targeting ``v3.6.x``) +still targeting ``v3.6.x``). + +Close milestones for versions that are unlikely to be released, e.g. micro versions of +older meso releases. Remilestone issues/PRs that are now untagged to the appropriate +future release milestone. Create the milestone for the next-next meso release (i.e. ``v3.9.0``, as ``v3.8.0`` should already exist). While most active items should go in the next meso release, @@ -125,22 +129,22 @@ prepare this list: 1. Archive the existing GitHub statistics page. - a. Copy the current :file:`doc/users/github_stats.rst` to - :file:`doc/users/prev_whats_new/github_stats_{X}.{Y}.{Z}.rst`. + a. Copy the current :file:`doc/release/github_stats.rst` to + :file:`doc/release/prev_whats_new/github_stats_{X}.{Y}.{Z}.rst`. b. Change the link target at the top of the file. c. Remove the "Previous GitHub Stats" section at the end. For example, when updating from v3.7.0 to v3.7.1:: - cp doc/users/github_stats.rst doc/users/prev_whats_new/github_stats_3.7.0.rst - $EDITOR doc/users/prev_whats_new/github_stats_3.7.0.rst + cp doc/release/github_stats.rst doc/release/prev_whats_new/github_stats_3.7.0.rst + $EDITOR doc/release/prev_whats_new/github_stats_3.7.0.rst # Change contents as noted above. - git add doc/users/prev_whats_new/github_stats_3.7.0.rst + git add doc/release/prev_whats_new/github_stats_3.7.0.rst 2. Re-generate the updated stats:: python tools/github_stats.py --since-tag v3.7.0 --milestone=v3.7.1 \ - --project 'matplotlib/matplotlib' --links > doc/users/github_stats.rst + --project 'matplotlib/matplotlib' --links > doc/release/github_stats.rst 3. Review and commit changes. Some issue/PR titles may not be valid reST (the most common issue is ``*`` which is interpreted as unclosed markup). Also confirm that @@ -194,8 +198,8 @@ What's new *Only needed for macro and meso releases. Bugfix releases should not have new features.* -Merge the contents of all the files in :file:`doc/users/next_whats_new/` into a single -file :file:`doc/users/prev_whats_new/whats_new_{X}.{Y}.0.rst` and delete the individual +Merge the contents of all the files in :file:`doc/release/next_whats_new/` into a single +file :file:`doc/release/prev_whats_new/whats_new_{X}.{Y}.0.rst` and delete the individual files. API changes @@ -211,7 +215,7 @@ individual files. Release notes TOC ^^^^^^^^^^^^^^^^^ -Update :file:`doc/users/release_notes.rst`: +Update :file:`doc/release/release_notes.rst`: - For macro and meso releases add a new section @@ -294,9 +298,15 @@ it is important to move all branches away from the commit with the tag [#]_:: git commit --allow-empty +Push the branch to GitHub. This is done prior to pushing the tag as a last step in ensuring +that the branch was fully up to date. If it fails, re-fetch and recreate commits and +tag over an up to date branch:: + + git push DANGER v3.7.x + Finally, push the tag to GitHub:: - git push DANGER v3.7.x v3.7.0 + git push DANGER v3.7.0 Congratulations, the scariest part is done! This assumes the release branch has already been made. @@ -373,12 +383,23 @@ Building binaries ================= We distribute macOS, Windows, and many Linux wheels as well as a source tarball via -PyPI. Most builders should trigger automatically once the tag is pushed to GitHub: +PyPI. * Windows, macOS and manylinux wheels are built on GitHub Actions. Builds are triggered - by the GitHub Action defined in :file:`.github/workflows/cibuildwheel.yml`, and wheels + by the GitHub Action defined in a separate + `release repository `__, and wheels will be available as artifacts of the build. Both a source tarball and the wheels will be automatically uploaded to PyPI once all of them have been built. +* To trigger the build for the ``matplotlib-release`` repository: + + * If not already created, create a release branch for the meso version (e.g. ``v3.10.x``) + * Edit the ``SOURCE_REF_TO_BUILD`` environment variable at the top of + `wheels.yml `__ + on the release branch to point to the release tag. + * Run the workflow from the release branch, with "pypi" selected for the pypi environment + using the `Workflow Dispatch trigger `__ + * This will run cibuildwheel and upload to PyPI using the Trusted Publishers GitHub Action. + * The auto-tick bot should open a pull request into the `conda-forge feedstock `__. Review and merge (if you have the power to). diff --git a/doc/devel/testing.rst b/doc/devel/testing.rst index 1fef85260b12..eae53c8602d4 100644 --- a/doc/devel/testing.rst +++ b/doc/devel/testing.rst @@ -274,14 +274,15 @@ You can also run tox on a subset of environments: $ tox -e py310,py311 -Tox processes everything serially so it can take a long time to test -several environments. To speed it up, you might try using a new, -parallelized version of tox called ``detox``. Give this a try: +Tox processes environments sequentially by default, +which can be slow when testing multiple environments. +To speed this up, tox now includes built-in parallelization support +via the --parallel flag. Give it a try: .. code-block:: bash - $ pip install -U -i http://pypi.testrun.org detox - $ detox + $ tox --parallel auto + Tox is configured using a file called ``tox.ini``. You may need to edit this file if you want to add new environments to test (e.g., diff --git a/doc/devel/troubleshooting.rst b/doc/devel/troubleshooting.rst index 74ce81b2da00..e57cfcb92bd6 100644 --- a/doc/devel/troubleshooting.rst +++ b/doc/devel/troubleshooting.rst @@ -23,7 +23,7 @@ mode:: git clean -xfd git pull python -m pip install -v . > build.out - python -c "from pylab import *; set_loglevel('debug'); plot(); show()" > run.out + python -c "from pylab import *; set_loglevel('DEBUG'); plot(); show()" > run.out and post :file:`build.out` and :file:`run.out` to the `matplotlib-devel `_ diff --git a/doc/index.rst b/doc/index.rst index 74a183d6cd7b..e77f405abb4b 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -14,59 +14,12 @@ and interactive visualizations. Install ======= -.. tab-set:: - :class: sd-width-content-min +.. include:: install/quick_install.inc.rst - .. tab-item:: pip +.. toctree:: + :hidden: - .. code-block:: bash - - pip install matplotlib - - .. tab-item:: conda - - .. code-block:: bash - - conda install -c conda-forge matplotlib - - .. tab-item:: pixi - - .. code-block:: bash - - pixi add matplotlib - - .. tab-item:: uv - - .. code-block:: bash - - uv add matplotlib - - .. warning:: - - If you install Python with ``uv`` then the ``tkagg`` backend - will not be available because python-build-standalone (used by uv - to distribute Python) does not contain tk bindings that are usable by - Matplotlib (see `this issue`_ for details). If you want Matplotlib - to be able to display plots in a window, you should install one of - the other :ref:`supported GUI frameworks `, - e.g. - - .. code-block:: bash - - uv add matplotlib pyside6 - - .. _this issue: https://github.com/astral-sh/uv/issues/6893#issuecomment-2565965851 - - .. tab-item:: other - - .. rst-class:: section-toc - .. toctree:: - :maxdepth: 2 - - install/index - -For more detailed instructions, see the -:doc:`installation guide `. + install/index Learn ===== @@ -163,7 +116,7 @@ What's new .. toctree:: :maxdepth: 1 - users/release_notes.rst + release/release_notes.rst Contribute diff --git a/doc/install/dependencies.rst b/doc/install/dependencies.rst index 4b006d9016e2..d0567900ea4c 100644 --- a/doc/install/dependencies.rst +++ b/doc/install/dependencies.rst @@ -31,6 +31,16 @@ reference. * `Pillow `_ (>= 9.0) * `pyparsing `_ (>= 3) +.. note:: + + With **conda packages**, this set of minimal dependencies is realized in + the ``matplotlib-base`` conda package. Other packages in the conda + ecosystem that depend on Matplotlib should depend on ``matplotlib-base``. + + The ``matplotlib`` conda package additionally comes with ``pyside6`` + to have a working GUI backend out of the box for end users. This should + primarily be used to define end-user environments. See also the + `conda forge documentation `__. .. _optional_dependencies: @@ -220,7 +230,7 @@ Build dependencies Python ------ -``pip`` normally builds packages using :external+pip:doc:`build isolation `, +``pip`` normally builds packages using :external+pip:doc:`build isolation `, which means that ``pip`` installs the dependencies listed here for the duration of the build process. However, build isolation is disabled via the the :external+pip:ref:`--no-build-isolation ` flag @@ -234,7 +244,7 @@ means that the dependencies must be explicitly installed, either by :ref:`creati - `setuptools_scm `_ (>= 7). Used to update the reported ``mpl.__version__`` based on the current git commit. Also a runtime dependency for editable installs. -- `NumPy `_ (>= 1.22). Also a runtime dependency. +- NumPy_ (>= 1.22). Also a runtime dependency. .. _compile-build-dependencies: @@ -377,7 +387,7 @@ them will be skipped by pytest. .. _pandas: https://pypi.org/project/pandas/ .. _pikepdf: https://pypi.org/project/pikepdf/ .. _psutil: https://pypi.org/project/psutil/ -.. _pytz: https://fonts.google.com/noto/use#faq +.. _pytz: https://pypi.org/project/pytz/ .. _pytest-cov: https://pytest-cov.readthedocs.io/en/latest/ .. _pytest-timeout: https://pypi.org/project/pytest-timeout/ .. _pytest-xdist: https://pypi.org/project/pytest-xdist/ @@ -473,7 +483,7 @@ Optional The documentation can be built without Inkscape and optipng, but the build process will raise various warnings. -* `Inkscape `_ +* Inkscape_ * `optipng `_ * the font `xkcd script `_ or `Comic Neue `_ * the font "Times New Roman" diff --git a/doc/install/index.rst b/doc/install/index.rst index 3e6452eb2f41..68ccfb8634ff 100644 --- a/doc/install/index.rst +++ b/doc/install/index.rst @@ -1,19 +1,22 @@ .. redirect-from:: /users/installing .. redirect-from:: /users/installing/index +.. highlight:: sh + ************ Installation ************ +.. include:: quick_install.inc.rst + +.. _install-official: Install an official release =========================== Matplotlib releases are available as wheel packages for macOS, Windows and Linux on `PyPI `_. Install it using -``pip``: - -.. code-block:: sh +``pip``:: python -m pip install -U pip python -m pip install -U matplotlib @@ -25,16 +28,26 @@ precompiled wheel for your OS and Python. .. note:: - The following backends work out of the box: Agg, ps, pdf, svg + The following non-interactive backends work out of the box: Agg, + ps, pdf, svg - Python is typically shipped with tk bindings which are used by - TkAgg. Notably, python-build-standalone – used by ``uv`` – does - not include tk bindings that are usable by Matplotlib. + The TkAgg interactive backend also typically works out of the box. + It requires Tk bindings, which are usually provided via the Python + standard library's ``tkinter`` module. On some OSes, you may need + to install a separate package like ``python3-tk`` to add this + component of the standard library. + + Some tools like ``uv`` make use of Python builds from the + python-build-standalone project, which only gained usable Tk + bindings recently (August 2025). If you are having trouble with the + TkAgg backend, ensure you have an up-to-date build, e.g. ``uv self + update && uv python upgrade --reinstall``. For support of other GUI frameworks, LaTeX rendering, saving animations and a larger selection of file formats, you can install :ref:`optional dependencies `. +.. _install-third-party: Third-party distributions ========================= @@ -44,15 +57,11 @@ Various third-parties provide Matplotlib for their environments. Conda packages -------------- -Matplotlib is available both via the *anaconda main channel* - -.. code-block:: sh +Matplotlib is available both via the *anaconda main channel* :: conda install matplotlib -as well as via the *conda-forge community channel* - -.. code-block:: sh +as well as via the *conda-forge community channel* :: conda install -c conda-forge matplotlib @@ -61,11 +70,9 @@ Python distributions Matplotlib is part of major Python distributions: -- `Anaconda `_ - +- Anaconda_ - `ActiveState ActivePython `_ - - `WinPython `_ Linux package manager @@ -81,7 +88,7 @@ you can install Matplotlib via your package manager, e.g.: .. redirect-from:: /users/installing/installing_source -.. _install_from_source: +.. _install-nightly-build: Install a nightly build ======================= @@ -90,9 +97,7 @@ Matplotlib makes nightly development build wheels available on the `scientific-python-nightly-wheels Anaconda Cloud organization `_. These wheels can be installed with ``pip`` by specifying -scientific-python-nightly-wheels as the package index to query: - -.. code-block:: sh +scientific-python-nightly-wheels as the package index to query:: python -m pip install \ --upgrade \ @@ -101,6 +106,7 @@ scientific-python-nightly-wheels as the package index to query: --extra-index-url https://pypi.org/simple \ matplotlib +.. _install-source: Install from source =================== @@ -143,8 +149,7 @@ Aspects of some behavioral defaults of the library can be configured via: environment_variables_faq.rst Default plotting appearance and behavior can be configured via the -:ref:`rcParams file ` - +:ref:`rcParams file `. Dependencies ============ @@ -179,7 +184,7 @@ development environment such as :program:`IDLE` which add additional complexities. Open up a UNIX shell or a DOS command prompt and run, for example:: - python -c "from pylab import *; set_loglevel('debug'); plot(); show()" + python -c "from pylab import *; set_loglevel('DEBUG'); plot(); show()" This will give you additional information about which backends Matplotlib is loading, version information, and more. At this point you might want to make @@ -266,13 +271,17 @@ at the Terminal.app command line:: python3 -c 'import matplotlib; print(matplotlib.__version__, matplotlib.__file__)' -You should see something like :: +You should see something like + +.. code-block:: none 3.10.0 /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages/matplotlib/__init__.py where ``3.10.0`` is the Matplotlib version you just installed, and the path following depends on whether you are using Python.org Python, Homebrew or -Macports. If you see another version, or you get an error like :: +Macports. If you see another version, or you get an error like + +.. code-block:: none Traceback (most recent call last): File "", line 1, in diff --git a/doc/install/quick_install.inc.rst b/doc/install/quick_install.inc.rst new file mode 100644 index 000000000000..0604a3c8fe75 --- /dev/null +++ b/doc/install/quick_install.inc.rst @@ -0,0 +1,53 @@ +.. set of quick install commands for reuse across docs + +.. tab-set:: + :class: sd-width-content-min + + .. tab-item:: pip + + .. code-block:: bash + + pip install matplotlib + + .. tab-item:: conda + + .. code-block:: bash + + conda install -c conda-forge matplotlib + + .. tab-item:: pixi + + .. code-block:: bash + + pixi add matplotlib + + .. tab-item:: uv + + .. code-block:: bash + + uv add matplotlib + + .. warning:: + + uv usually installs its own versions of Python from the + python-build-standalone project, and only recent versions of those + Python builds (August 2025) work properly with the ``tkagg`` backend + for displaying plots in a window. Please make sure you are using uv + 0.8.7 or newer (update with e.g. ``uv self update``) and that your + bundled Python installs are up to date (with ``uv python upgrade + --reinstall``). Alternatively, you can use one of the other + :ref:`supported GUI frameworks `, e.g. + + .. code-block:: bash + + uv add matplotlib pyside6 + + .. tab-item:: other + + :ref:`install-official` + + :ref:`install-third-party` + + :ref:`install-nightly-build` + + :ref:`install-source` diff --git a/doc/install/troubleshooting_faq.inc.rst b/doc/install/troubleshooting_faq.inc.rst index d130813a80c6..fce94cef6a66 100644 --- a/doc/install/troubleshooting_faq.inc.rst +++ b/doc/install/troubleshooting_faq.inc.rst @@ -11,12 +11,11 @@ Obtaining Matplotlib version ---------------------------- To find out your Matplotlib version number, import it and print the -``__version__`` attribute:: - - >>> import matplotlib - >>> matplotlib.__version__ - '0.98.0' +``__version__`` attribute: +>>> import matplotlib +>>> matplotlib.__version__ +'0.98.0' .. _locating-matplotlib-install: @@ -24,12 +23,11 @@ To find out your Matplotlib version number, import it and print the ----------------------------------- You can find what directory Matplotlib is installed in by importing it -and printing the ``__file__`` attribute:: - - >>> import matplotlib - >>> matplotlib.__file__ - '/home/jdhunter/dev/lib64/python2.5/site-packages/matplotlib/__init__.pyc' +and printing the ``__file__`` attribute: +>>> import matplotlib +>>> matplotlib.__file__ +'/home/jdhunter/dev/lib64/python2.5/site-packages/matplotlib/__init__.pyc' .. _locating-matplotlib-config-dir: @@ -39,32 +37,32 @@ and printing the ``__file__`` attribute:: Each user has a Matplotlib configuration directory which may contain a :ref:`matplotlibrc ` file. To locate your :file:`matplotlib/` configuration directory, use -:func:`matplotlib.get_configdir`:: +:func:`matplotlib.get_configdir`: - >>> import matplotlib as mpl - >>> mpl.get_configdir() - '/home/darren/.config/matplotlib' +>>> import matplotlib as mpl +>>> mpl.get_configdir() +'/home/darren/.config/matplotlib' On Unix-like systems, this directory is generally located in your :envvar:`HOME` directory under the :file:`.config/` directory. In addition, users have a cache directory. On Unix-like systems, this is separate from the configuration directory by default. To locate your -:file:`.cache/` directory, use :func:`matplotlib.get_cachedir`:: +:file:`.cache/` directory, use :func:`matplotlib.get_cachedir`: - >>> import matplotlib as mpl - >>> mpl.get_cachedir() - '/home/darren/.cache/matplotlib' +>>> import matplotlib as mpl +>>> mpl.get_cachedir() +'/home/darren/.cache/matplotlib' On Windows, both the config directory and the cache directory are the same and are in your :file:`Documents and Settings` or :file:`Users` -directory by default:: +directory by default: - >>> import matplotlib as mpl - >>> mpl.get_configdir() - 'C:\\Documents and Settings\\jdhunter\\.matplotlib' - >>> mpl.get_cachedir() - 'C:\\Documents and Settings\\jdhunter\\.matplotlib' +>>> import matplotlib as mpl +>>> mpl.get_configdir() +'C:\\Documents and Settings\\jdhunter\\.matplotlib' +>>> mpl.get_cachedir() +'C:\\Documents and Settings\\jdhunter\\.matplotlib' If you would like to use a different configuration directory, you can do so by specifying the location in your :envvar:`MPLCONFIGDIR` diff --git a/doc/missing-references.json b/doc/missing-references.json index 1a3693c990e5..d27d2fa067ce 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -4,7 +4,7 @@ ":1" ], "matplotlib.axes._base._AxesBase": [ - "doc/api/artist_api.rst:202" + "doc/api/artist_api.rst:203" ], "matplotlib.backend_bases._Backend": [ "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ShowBase:1" @@ -18,7 +18,7 @@ "lib/matplotlib/backends/backend_tkcairo.py:docstring of matplotlib.backends.backend_tkcairo.FigureCanvasTkCairo:1" ], "matplotlib.image._ImageBase": [ - "doc/api/artist_api.rst:202", + "doc/api/artist_api.rst:203", "lib/matplotlib/image.py:docstring of matplotlib.image.AxesImage:1", "lib/matplotlib/image.py:docstring of matplotlib.image.BboxImage:1", "lib/matplotlib/image.py:docstring of matplotlib.image.FigureImage:1" @@ -67,7 +67,7 @@ "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.MollweideAxes.MollweideTransform:1" ], "matplotlib.text._AnnotationBase": [ - "doc/api/artist_api.rst:202", + "doc/api/artist_api.rst:203", "lib/matplotlib/offsetbox.py:docstring of matplotlib.offsetbox.AnnotationBbox:1", "lib/matplotlib/text.py:docstring of matplotlib.text.Annotation:1" ], diff --git a/doc/project/citing.rst b/doc/project/citing.rst index 2cd317906bb5..c5e56e6f12d4 100644 --- a/doc/project/citing.rst +++ b/doc/project/citing.rst @@ -32,6 +32,21 @@ By version .. START OF AUTOGENERATED +v3.10.7 + .. image:: ../_static/zenodo_cache/17298696.svg + :target: https://doi.org/10.5281/zenodo.17298696 +v3.10.6 + .. image:: ../_static/zenodo_cache/16999430.svg + :target: https://doi.org/10.5281/zenodo.16999430 +v3.10.5 + .. image:: ../_static/zenodo_cache/16644850.svg + :target: https://doi.org/10.5281/zenodo.16644850 +v3.10.3 + .. image:: ../_static/zenodo_cache/15375714.svg + :target: https://doi.org/10.5281/zenodo.15375714 +v3.10.1 + .. image:: ../_static/zenodo_cache/14940554.svg + :target: https://doi.org/10.5281/zenodo.14940554 v3.10.0 .. image:: ../_static/zenodo_cache/14464227.svg :target: https://doi.org/10.5281/zenodo.14464227 diff --git a/doc/users/generate_credits.py b/doc/project/generate_credits.py similarity index 100% rename from doc/users/generate_credits.py rename to doc/project/generate_credits.py diff --git a/doc/release/github_stats.rst b/doc/release/github_stats.rst new file mode 100644 index 000000000000..cd9e71ff9376 --- /dev/null +++ b/doc/release/github_stats.rst @@ -0,0 +1,90 @@ +.. redirect-from:: /users/github_stats + +.. _github-stats: + +GitHub statistics for 3.10.8 (Nov 12, 2025) +=========================================== + +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2025/11/12 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 4 issues and merged 16 pull requests. +The full list can be seen `on GitHub `__ + +The following 35 authors contributed 445 commits. + +* Aasma Gupta +* Antony Lee +* Christine P. Chai +* David Stansby +* dependabot[bot] +* Elliott Sales de Andrade +* G.D. McBain +* Greg Lucas +* hannah +* heinrich5991 +* hu-xiaonan +* Ian Thomas +* Inês Cachola +* Jody Klymak +* Jouni K. Seppänen +* Khushi_29 +* Kyle Sunden +* Lucas Gruwez +* Lumberbot (aka Jack) +* N R Navaneet +* Nathan G. Wiseman +* Nathan Goldbaum +* Nick Coish +* Oscar Gustafsson +* Praful Gulani +* Qian Zhang +* Rafael Katri +* Raphael Erik Hviding +* Roman +* Ruth Comer +* saikarna913 +* Scott Shambaugh +* Thomas A Caswell +* Tim Hoffmann +* Trygve Magnus Ræder + +GitHub issues and pull requests: + +Pull Requests (16): + +* :ghpull:`30717`: Backport PR #30714 on branch v3.10.x (FIX: Gracefully handle numpy arrays as input to check_in_list()) +* :ghpull:`30714`: FIX: Gracefully handle numpy arrays as input to check_in_list() +* :ghpull:`30560`: Consistent zoom boxes +* :ghpull:`30711`: Backport PR #30697 on branch v3.10.x (BUG: raise when creating a MacOS FigureManager outside the main thread) +* :ghpull:`30697`: BUG: raise when creating a MacOS FigureManager outside the main thread +* :ghpull:`30656`: Backport PR #29810 on branch v3.10.x (Declare free-threaded support in MacOS backend extension) +* :ghpull:`30702`: Backport PR #30624 on branch v3.10.x (TST: Increase tolerances for Ghostscript 10.06) +* :ghpull:`30700`: Backport PR #30698 on branch v3.10.x (BLD: update trove metadata to support py3.14) +* :ghpull:`30624`: TST: Increase tolerances for Ghostscript 10.06 +* :ghpull:`30698`: BLD: update trove metadata to support py3.14 +* :ghpull:`30688`: Backport PR #30687 on branch v3.10.x (DOC: Fix pip link) +* :ghpull:`30675`: Backport PR #30657 on branch v3.10.x (Fix AttributeError: module 'gi' has no attribute 'require_version') +* :ghpull:`30674`: Backport PR #30672 on branch v3.10.x (Use pathlib.Path instead of matplotlib.path.Path in text.pyi) +* :ghpull:`30672`: Use pathlib.Path instead of matplotlib.path.Path in text.pyi +* :ghpull:`30657`: Fix ``AttributeError: module 'gi' has no attribute 'require_version'`` +* :ghpull:`29810`: Declare free-threaded support in MacOS backend extension + +Issues (4): + +* :ghissue:`30706`: [Bug]: Axes.grouped_bar() with non-string orientation (e.g., NumPy array) raises ambiguous truth-value error instead of clean ValueError +* :ghissue:`30666`: [Bug]: calling pyplot.gca() outside the main thread crashes the interpreter with the MacOS backend +* :ghissue:`30669`: [Bug]: Type hint for fontproperties keyword in text.pyi is wrong +* :ghissue:`30654`: [Bug]: error plotting: AttributeError: module 'gi' has no attribute 'require_version' + + +Previous GitHub statistics +-------------------------- + +.. toctree:: + :maxdepth: 1 + :glob: + :reversed: + + prev_whats_new/github_stats_* diff --git a/doc/users/next_whats_new.rst b/doc/release/next_whats_new.rst similarity index 80% rename from doc/users/next_whats_new.rst rename to doc/release/next_whats_new.rst index ddd82faf6731..7923dde4722a 100644 --- a/doc/users/next_whats_new.rst +++ b/doc/release/next_whats_new.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/next_whats_new + .. _whats-new: ================ diff --git a/doc/users/next_whats_new/3d_speedups.rst b/doc/release/next_whats_new/3d_speedups.rst similarity index 100% rename from doc/users/next_whats_new/3d_speedups.rst rename to doc/release/next_whats_new/3d_speedups.rst diff --git a/doc/users/next_whats_new/README.rst b/doc/release/next_whats_new/README.rst similarity index 91% rename from doc/users/next_whats_new/README.rst rename to doc/release/next_whats_new/README.rst index 23efd0208edb..1a7039a4a5db 100644 --- a/doc/users/next_whats_new/README.rst +++ b/doc/release/next_whats_new/README.rst @@ -15,7 +15,7 @@ Each new feature (e.g. function, parameter, config value, behavior, ...) must be described through a "What's new" entry. Each entry is written into a separate file in the -:file:`doc/users/next_whats_new/` directory. They are sorted and merged into +:file:`doc/release/next_whats_new/` directory. They are sorted and merged into :file:`whats_new.rst` during the release process. When adding an entry please look at the currently existing files to @@ -34,7 +34,7 @@ Include contents of the form:: details should be left out when they do not impact usage, for example implementation details. - The description may include a a short instructive example, if it helps to + The description may include a short instructive example, if it helps to understand the feature. Please avoid using references in section titles, as it causes links to be diff --git a/doc/users/next_whats_new/axis_inversion.rst b/doc/release/next_whats_new/axis_inversion.rst similarity index 100% rename from doc/users/next_whats_new/axis_inversion.rst rename to doc/release/next_whats_new/axis_inversion.rst diff --git a/doc/users/next_whats_new/bar_label_padding_update.rst b/doc/release/next_whats_new/bar_label_padding_update.rst similarity index 100% rename from doc/users/next_whats_new/bar_label_padding_update.rst rename to doc/release/next_whats_new/bar_label_padding_update.rst diff --git a/doc/release/next_whats_new/barcontainer_properties.rst b/doc/release/next_whats_new/barcontainer_properties.rst new file mode 100644 index 000000000000..0efe4ee00e4f --- /dev/null +++ b/doc/release/next_whats_new/barcontainer_properties.rst @@ -0,0 +1,7 @@ +``BarContainer`` properties +--------------------------- +`.BarContainer` gained new properties to easily access coordinates of the bars: + +- `~.BarContainer.bottoms` +- `~.BarContainer.tops` +- `~.BarContainer.position_centers` diff --git a/doc/release/next_whats_new/broken_barh_align.rst b/doc/release/next_whats_new/broken_barh_align.rst new file mode 100644 index 000000000000..5108ac5b0e9a --- /dev/null +++ b/doc/release/next_whats_new/broken_barh_align.rst @@ -0,0 +1,4 @@ +``broken_barh()`` vertical alignment though ``align`` parameter +--------------------------------------------------------------- +`~.Axes.broken_barh` now supports vertical alignment of the bars through the +``align`` parameter. diff --git a/doc/users/next_whats_new/color_cycle_from_sequence.rst b/doc/release/next_whats_new/color_cycle_from_sequence.rst similarity index 100% rename from doc/users/next_whats_new/color_cycle_from_sequence.rst rename to doc/release/next_whats_new/color_cycle_from_sequence.rst diff --git a/doc/users/next_whats_new/colormap_bad_under_over.rst b/doc/release/next_whats_new/colormap_bad_under_over.rst similarity index 100% rename from doc/users/next_whats_new/colormap_bad_under_over.rst rename to doc/release/next_whats_new/colormap_bad_under_over.rst diff --git a/doc/users/next_whats_new/colormap_with_alpha b/doc/release/next_whats_new/colormap_with_alpha.rst similarity index 100% rename from doc/users/next_whats_new/colormap_with_alpha rename to doc/release/next_whats_new/colormap_with_alpha.rst diff --git a/doc/users/next_whats_new/depthshading_improvement.rst b/doc/release/next_whats_new/depthshading_improvement.rst similarity index 100% rename from doc/users/next_whats_new/depthshading_improvement.rst rename to doc/release/next_whats_new/depthshading_improvement.rst diff --git a/doc/users/next_whats_new/figsize_unit.rst b/doc/release/next_whats_new/figsize_unit.rst similarity index 100% rename from doc/users/next_whats_new/figsize_unit.rst rename to doc/release/next_whats_new/figsize_unit.rst diff --git a/doc/users/next_whats_new/gif_savefig.rst b/doc/release/next_whats_new/gif_savefig.rst similarity index 100% rename from doc/users/next_whats_new/gif_savefig.rst rename to doc/release/next_whats_new/gif_savefig.rst diff --git a/doc/users/next_whats_new/grouped_bar.rst b/doc/release/next_whats_new/grouped_bar.rst similarity index 100% rename from doc/users/next_whats_new/grouped_bar.rst rename to doc/release/next_whats_new/grouped_bar.rst diff --git a/doc/release/next_whats_new/hist_color.rst b/doc/release/next_whats_new/hist_color.rst new file mode 100644 index 000000000000..2d6a4adb3464 --- /dev/null +++ b/doc/release/next_whats_new/hist_color.rst @@ -0,0 +1,5 @@ +``hist()`` supports a single color for multiple datasets +-------------------------------------------------------- + +It is now possible to pass a single *color* value to `~.Axes.hist()`. This +value is applied to all datasets. diff --git a/doc/users/next_whats_new/last_resort_font.rst b/doc/release/next_whats_new/last_resort_font.rst similarity index 100% rename from doc/users/next_whats_new/last_resort_font.rst rename to doc/release/next_whats_new/last_resort_font.rst diff --git a/doc/release/next_whats_new/legend_line_width.rst b/doc/release/next_whats_new/legend_line_width.rst new file mode 100644 index 000000000000..d8cfd57640a8 --- /dev/null +++ b/doc/release/next_whats_new/legend_line_width.rst @@ -0,0 +1,21 @@ +``legend.linewidth`` rcParam and parameter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +A new rcParam ``legend.linewidth`` has been added to control the line width of +the legend's box edges. When set to ``None`` (the default), it inherits the +value from ``patch.linewidth``. This allows for independent control of the +legend frame line width without affecting other elements. + +The `.Legend` constructor also accepts a new *linewidth* parameter to set the +legend frame line width directly, overriding the rcParam value. + +.. plot:: + :include-source: true + :alt: A line plot with a legend showing a thick border around the legend box. + + import matplotlib.pyplot as plt + + fig, ax = plt.subplots() + ax.plot([1, 2, 3], label='data') + ax.legend(linewidth=2.0) # Thick legend box edge + plt.show() diff --git a/doc/users/next_whats_new/log_contour_levels.rst b/doc/release/next_whats_new/log_contour_levels.rst similarity index 100% rename from doc/users/next_whats_new/log_contour_levels.rst rename to doc/release/next_whats_new/log_contour_levels.rst diff --git a/doc/users/next_whats_new/logticks.rst b/doc/release/next_whats_new/logticks.rst similarity index 100% rename from doc/users/next_whats_new/logticks.rst rename to doc/release/next_whats_new/logticks.rst diff --git a/doc/users/next_whats_new/new_rcparams_grid_options.rst b/doc/release/next_whats_new/new_rcparams_grid_options.rst similarity index 100% rename from doc/users/next_whats_new/new_rcparams_grid_options.rst rename to doc/release/next_whats_new/new_rcparams_grid_options.rst diff --git a/doc/release/next_whats_new/okabe_ito_colormap.rst b/doc/release/next_whats_new/okabe_ito_colormap.rst new file mode 100644 index 000000000000..fad461257932 --- /dev/null +++ b/doc/release/next_whats_new/okabe_ito_colormap.rst @@ -0,0 +1,29 @@ +Okabe-Ito accessible color sequence +----------------------------------- + +Matplotlib now includes the `Okabe-Ito color sequence`_. Its colors remain distinguishable for common forms of color-vision deficiency and when printed. + +.. _Okabe-Ito color sequence: https://jfly.uni-koeln.de/color/#pallet + +For example, to set it as the default colormap for your plots and image-like artists, use: + +.. code-block:: python + + import matplotlib.pyplot as plt + from cycler import cycler + + plt.rcParams['axes.prop_cycle'] = cycler('color', plt.colormaps['okabe_ito'].colors) + plt.rcParams['image.cmap'] = 'okabe_ito' + +Or, when creating plots, you can pass it explicitly: + +.. plot:: + + import matplotlib.pyplot as plt + + colors = plt.colormaps['okabe_ito'].colors + x = range(5) + for i, c in enumerate(colors): + plt.plot(x, [v*(i+1) for v in x], color=c, label=f'line {i}') + plt.legend() + plt.show() diff --git a/doc/release/next_whats_new/patchcollection_legend.rst b/doc/release/next_whats_new/patchcollection_legend.rst new file mode 100644 index 000000000000..58574e9e6757 --- /dev/null +++ b/doc/release/next_whats_new/patchcollection_legend.rst @@ -0,0 +1,22 @@ +``PatchCollection`` legends now supported +------------------------------------------ +`.PatchCollection` instances now properly display in legends when given a label. +Previously, labels on `~.PatchCollection` objects were ignored by the legend +system, requiring users to create manual legend entries. + +.. plot:: + :include-source: true + :alt: The legend entry displays a rectangle matching the visual properties (colors, line styles, line widths) of the first patch in the collection. + + import matplotlib.pyplot as plt + import matplotlib.patches as mpatches + from matplotlib.collections import PatchCollection + + fig, ax = plt.subplots() + patches = [mpatches.Circle((0, 0), 0.1), mpatches.Rectangle((0.5, 0.5), 0.2, 0.3)] + pc = PatchCollection(patches, facecolor='blue', edgecolor='black', label='My patches') + ax.add_collection(pc) + ax.legend() # Now displays the label "My patches" + plt.show() + +This fix resolves :ghissue:`23998`. diff --git a/doc/release/next_whats_new/pie_label.rst b/doc/release/next_whats_new/pie_label.rst new file mode 100644 index 000000000000..6dc9a3f619c2 --- /dev/null +++ b/doc/release/next_whats_new/pie_label.rst @@ -0,0 +1,28 @@ +Adding labels to pie chart wedges +--------------------------------- + +The new `~.Axes.pie_label` method adds a label to each wedge in a pie chart created with +`~.Axes.pie`. It can take + +* a list of strings, similar to the existing *labels* parameter of `~.Axes.pie` +* a format string similar to the existing *autopct* parameter of `~.Axes.pie` except + that it uses the `str.format` method and it can handle absolute values as well as + fractions/percentages + +For more examples, see :doc:`/gallery/pie_and_polar_charts/pie_label`. + +.. plot:: + :include-source: true + :alt: A pie chart with three labels on each wedge, showing a food type, number, and fraction associated with the wedge. + + import matplotlib.pyplot as plt + + data = [36, 24, 8, 12] + labels = ['spam', 'eggs', 'bacon', 'sausage'] + + fig, ax = plt.subplots() + pie = ax.pie(data) + + ax.pie_label(pie, labels, distance=1.1) + ax.pie_label(pie, '{frac:.1%}', distance=0.7) + ax.pie_label(pie, '{absval:d}', distance=0.4) diff --git a/doc/release/next_whats_new/pyplot-register-figure.rst b/doc/release/next_whats_new/pyplot-register-figure.rst new file mode 100644 index 000000000000..86ffcbf2294a --- /dev/null +++ b/doc/release/next_whats_new/pyplot-register-figure.rst @@ -0,0 +1,63 @@ +Figures can be attached to and removed from pyplot +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Figures can now be attached to and removed from management through pyplot, which in +the background also means a less strict coupling to backends. + +In particular, standalone figures (created with the `.Figure` constructor) can now be +registered with the `.pyplot` module by calling ``plt.figure(fig)``. This allows to +show them with ``plt.show()`` as you would do with any figure created with pyplot +factory methods such as ``plt.figure()`` or ``plt.subplots()``. + +When closing a shown figure window, the related figure is reset to the standalone +state, i.e. it's not visible to pyplot anymore, but if you still hold a reference +to it, you can continue to work with it (e.g. do ``fig.savefig()``, or re-add it +to pyplot with ``plt.figure(fig)`` and then show it again). + +The following is now possible - though the example is exaggerated to show what's +possible. In practice, you'll stick with much simpler versions for better +consistency :: + + import matplotlib.pyplot as plt + from matplotlib.figure import Figure + + # Create a standalone figure + fig = Figure() + ax = fig.add_subplot() + ax.plot([1, 2, 3], [4, 5, 6]) + + # Register it with pyplot + plt.figure(fig) + + # Modify the figure through pyplot + plt.xlabel("x label") + + # Show the figure + plt.show() + + # Close the figure window through the GUI + + # Continue to work on the figure + fig.savefig("my_figure.png") + ax.set_ylabel("y label") + + # Re-register the figure and show it again + plt.figure(fig) + plt.show() + +Technical detail: Standalone figures use `.FigureCanvasBase` as canvas. This is +replaced by a backend-dependent subclass when registering with pyplot, and is +reset to `.FigureCanvasBase` when the figure is closed. `.Figure.savefig` uses +the current canvas to save the figure (if possible). Since `.FigureCanvasBase` +can not render the figure, when saving the figure, it will fallback to a suitable +canvas subclass, e.g. `.FigureCanvasAgg` for raster outputs such as png. +Any Agg-based backend will create the same file output. However, there may be +slight differences for non-Agg backends; e.g. if you use "GTK4Cairo" as +interactive backend, ``fig.savefig("file.png")`` may create a slightly different +image depending on whether the figure is registered with pyplot or not. In +general, you should not store a reference to the canvas, but rather always +obtain it from the figure with ``fig.canvas``. This will return the current +canvas, which is either the original `.FigureCanvasBase` or a backend-dependent +subclass, depending on whether the figure is registered with pyplot or not. +Additionally, the swapping of the canvas currently does not play well with +blitting of matplotlib widgets; in such cases either deactivate blitting or do not +continue to use the figure (e.g. saving it after closing the window). diff --git a/doc/release/next_whats_new/scroll_to_zoom.rst b/doc/release/next_whats_new/scroll_to_zoom.rst new file mode 100644 index 000000000000..1be522b7a255 --- /dev/null +++ b/doc/release/next_whats_new/scroll_to_zoom.rst @@ -0,0 +1,9 @@ +Zooming using mouse wheel +~~~~~~~~~~~~~~~~~~~~~~~~~ + +``Ctrl+MouseWheel`` can be used to zoom in the plot windows. +Additionally, ``x+MouseWheel`` zooms only the x-axis and ``y+MouseWheel`` zooms only the y-axis. + +The zoom focusses on the mouse pointer. With ``Ctrl``, the axes aspect ratio is kept; with ``x`` or ``y``, only the respective axis is scaled. + +Zooming is currently only supported on rectilinear Axes. diff --git a/doc/users/next_whats_new/separated_hatchcolor.rst b/doc/release/next_whats_new/separated_hatchcolor.rst similarity index 100% rename from doc/users/next_whats_new/separated_hatchcolor.rst rename to doc/release/next_whats_new/separated_hatchcolor.rst diff --git a/doc/users/next_whats_new/six_and_eight_color_petroff_color_cycles.rst b/doc/release/next_whats_new/six_and_eight_color_petroff_color_cycles.rst similarity index 100% rename from doc/users/next_whats_new/six_and_eight_color_petroff_color_cycles.rst rename to doc/release/next_whats_new/six_and_eight_color_petroff_color_cycles.rst diff --git a/doc/release/next_whats_new/sliders_callable_valfmt.rst b/doc/release/next_whats_new/sliders_callable_valfmt.rst new file mode 100644 index 000000000000..1d350dba348a --- /dev/null +++ b/doc/release/next_whats_new/sliders_callable_valfmt.rst @@ -0,0 +1,6 @@ +Callable *valfmt* for ``Slider`` and ``RangeSlider`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In addition to the existing %-format string, the *valfmt* parameter of +`~.matplotlib.widgets.Slider` and `~.matplotlib.widgets.RangeSlider` now +also accepts a callable of the form ``valfmt(val: float) -> str``. diff --git a/doc/release/next_whats_new/stackplot_style_sequences.rst b/doc/release/next_whats_new/stackplot_style_sequences.rst new file mode 100644 index 000000000000..209d30a15218 --- /dev/null +++ b/doc/release/next_whats_new/stackplot_style_sequences.rst @@ -0,0 +1,6 @@ +Stackplot styling +----------------- + +`~.Axes.stackplot` now accepts sequences for the style parameters *facecolor*, +*edgecolor*, *linestyle*, and *linewidth*, similar to how the *hatch* parameter +is already handled. diff --git a/doc/users/next_whats_new/streamplot_integration_control.rst b/doc/release/next_whats_new/streamplot_integration_control.rst similarity index 100% rename from doc/users/next_whats_new/streamplot_integration_control.rst rename to doc/release/next_whats_new/streamplot_integration_control.rst diff --git a/doc/users/next_whats_new/streamplot_multiple_arrows.rst b/doc/release/next_whats_new/streamplot_multiple_arrows.rst similarity index 100% rename from doc/users/next_whats_new/streamplot_multiple_arrows.rst rename to doc/release/next_whats_new/streamplot_multiple_arrows.rst diff --git a/doc/users/next_whats_new/subplots_adjust.rst b/doc/release/next_whats_new/subplots_adjust.rst similarity index 100% rename from doc/users/next_whats_new/subplots_adjust.rst rename to doc/release/next_whats_new/subplots_adjust.rst diff --git a/doc/users/next_whats_new/type1_subset.rst b/doc/release/next_whats_new/type1_subset.rst similarity index 100% rename from doc/users/next_whats_new/type1_subset.rst rename to doc/release/next_whats_new/type1_subset.rst diff --git a/doc/release/next_whats_new/updated_borderpad_parameter.rst b/doc/release/next_whats_new/updated_borderpad_parameter.rst new file mode 100644 index 000000000000..5acf075f7b51 --- /dev/null +++ b/doc/release/next_whats_new/updated_borderpad_parameter.rst @@ -0,0 +1,18 @@ +``borderpad`` accepts a tuple for separate x/y padding +------------------------------------------------------- + +The ``borderpad`` parameter used for placing anchored artists (such as inset axes) now accepts a tuple of ``(x_pad, y_pad)``. + +This allows for specifying separate padding values for the horizontal and +vertical directions, providing finer control over placement. For example, when +placing an inset in a corner, one might want horizontal padding to avoid +overlapping with the main plot's axis labels, but no vertical padding to keep +the inset flush with the plot area edge. + +Example usage with :func:`~mpl_toolkits.axes_grid1.inset_locator.inset_axes`: + +.. code-block:: python + + ax_inset = inset_axes( + ax, width="30%", height="30%", loc='upper left', + borderpad=(4, 0)) diff --git a/doc/release/next_whats_new/violin_stats.rst b/doc/release/next_whats_new/violin_stats.rst new file mode 100644 index 000000000000..a764db01da59 --- /dev/null +++ b/doc/release/next_whats_new/violin_stats.rst @@ -0,0 +1,31 @@ +``violin_stats`` simpler *method* parameter +------------------------------------------- + +The *method* parameter of `~.cbook.violin_stats` may now be specified as tuple of +strings, and has a new default ``("GaussianKDE", "scott")``. Calling +`~.cbook.violin_stats` followed by `~.Axes.violin` is therefore now equivalent to +calling `~.Axes.violinplot`. + +.. plot:: + :include-source: true + :alt: Example showing violin_stats followed by violin gives the same result as violinplot + + import matplotlib.pyplot as plt + from matplotlib.cbook import violin_stats + import numpy as np + + rng = np.random.default_rng(19680801) + data = rng.normal(size=(10, 3)) + + fig, (ax1, ax2) = plt.subplots(ncols=2, layout='constrained', figsize=(6.4, 3.5)) + + # Create the violin plot in one step + ax1.violinplot(data) + ax1.set_title('One Step') + + # Process the data and then create the violin plot + vstats = violin_stats(data) + ax2.violin(vstats) + ax2.set_title('Two Steps') + + plt.show() diff --git a/doc/users/next_whats_new/violinplot_colors.rst b/doc/release/next_whats_new/violinplot_colors.rst similarity index 100% rename from doc/users/next_whats_new/violinplot_colors.rst rename to doc/release/next_whats_new/violinplot_colors.rst diff --git a/doc/release/next_whats_new/webagg_capture_scroll.rst b/doc/release/next_whats_new/webagg_capture_scroll.rst new file mode 100644 index 000000000000..106e1ebdf36b --- /dev/null +++ b/doc/release/next_whats_new/webagg_capture_scroll.rst @@ -0,0 +1,7 @@ +WebAgg scroll capture control +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The WebAgg backend now provides the ability to capture scroll events to prevent +page scrolling when interacting with plots. This can be enabled or disabled via +the new `.FigureCanvasWebAggCore.set_capture_scroll` and +`.FigureCanvasWebAggCore.get_capture_scroll` methods. diff --git a/doc/users/next_whats_new/xtick_ytick_rotation_modes.rst b/doc/release/next_whats_new/xtick_ytick_rotation_modes.rst similarity index 100% rename from doc/users/next_whats_new/xtick_ytick_rotation_modes.rst rename to doc/release/next_whats_new/xtick_ytick_rotation_modes.rst diff --git a/doc/release/next_whats_new/zoom_boxes.rst b/doc/release/next_whats_new/zoom_boxes.rst new file mode 100644 index 000000000000..8cc0cc1645a3 --- /dev/null +++ b/doc/release/next_whats_new/zoom_boxes.rst @@ -0,0 +1,4 @@ +Consistent zoom boxes +--------------------- + +Zooming now has a consistent dashed box style across all backends. diff --git a/doc/users/prev_whats_new/changelog.rst b/doc/release/prev_whats_new/changelog.rst similarity index 99% rename from doc/users/prev_whats_new/changelog.rst rename to doc/release/prev_whats_new/changelog.rst index 8f505e4fdd37..47b1fb68b09a 100644 --- a/doc/users/prev_whats_new/changelog.rst +++ b/doc/release/prev_whats_new/changelog.rst @@ -1,10 +1,12 @@ +.. redirect-from:: /users/prev_whats_new/changelog + .. _old_changelog: List of changes to Matplotlib prior to 2015 =========================================== This is a list of the changes made to Matplotlib from 2003 to 2015. For more -recent changes, please refer to the :doc:`/users/release_notes`. +recent changes, please refer to the :doc:`/release/release_notes`. 2015-11-16 Levels passed to contour(f) and tricontour(f) must be in increasing order. @@ -1689,7 +1691,7 @@ recent changes, please refer to the :doc:`/users/release_notes`. required by the experimental traited config and are somewhat out of date. If needed, install them independently, see http://code.enthought.com/pages/traits.html and - http://www.voidspace.org.uk/python/configobj.html + https://configobj.readthedocs.io/en/latest/ 2008-12-12 Added support to assign labels to histograms of multiple data. - MM @@ -4272,7 +4274,7 @@ recent changes, please refer to the :doc:`/users/release_notes`. 2006-01-11 Released 0.86.1 -2006-01-11 +2006-01-11 Fixed setup.py for win32 build and added rc template to the MANIFEST.in 2006-01-10 diff --git a/doc/users/prev_whats_new/dflt_style_changes.rst b/doc/release/prev_whats_new/dflt_style_changes.rst similarity index 99% rename from doc/users/prev_whats_new/dflt_style_changes.rst rename to doc/release/prev_whats_new/dflt_style_changes.rst index 808204383fb8..e4697cf2c451 100644 --- a/doc/users/prev_whats_new/dflt_style_changes.rst +++ b/doc/release/prev_whats_new/dflt_style_changes.rst @@ -1,3 +1,4 @@ +.. redirect-from:: /users/prev_whats_new/dflt_style_changes .. redirect-from:: /users/dflt_style_changes ============================== diff --git a/doc/users/prev_whats_new/github_stats_3.0.0.rst b/doc/release/prev_whats_new/github_stats_3.0.0.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.0.0.rst rename to doc/release/prev_whats_new/github_stats_3.0.0.rst index cae3ee9b570d..dd17bc0fece7 100644 --- a/doc/users/prev_whats_new/github_stats_3.0.0.rst +++ b/doc/release/prev_whats_new/github_stats_3.0.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.0.0 + .. _github-stats-3-0-0: GitHub statistics for 3.0.0 (Sep 18, 2018) diff --git a/doc/users/prev_whats_new/github_stats_3.0.1.rst b/doc/release/prev_whats_new/github_stats_3.0.1.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.0.1.rst rename to doc/release/prev_whats_new/github_stats_3.0.1.rst index 8ebc7f5f11c1..eaa0f88ba22a 100644 --- a/doc/users/prev_whats_new/github_stats_3.0.1.rst +++ b/doc/release/prev_whats_new/github_stats_3.0.1.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.0.1 + .. _github-stats-3-0-1: GitHub statistics for 3.0.1 (Oct 25, 2018) diff --git a/doc/users/prev_whats_new/github_stats_3.0.2.rst b/doc/release/prev_whats_new/github_stats_3.0.2.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.0.2.rst rename to doc/release/prev_whats_new/github_stats_3.0.2.rst index 6b4ef3071f1c..45c99e990147 100644 --- a/doc/users/prev_whats_new/github_stats_3.0.2.rst +++ b/doc/release/prev_whats_new/github_stats_3.0.2.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.0.2 + .. _github-stats-3-0-2: GitHub statistics for 3.0.2 (Nov 10, 2018) diff --git a/doc/users/prev_whats_new/github_stats_3.0.3.rst b/doc/release/prev_whats_new/github_stats_3.0.3.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.0.3.rst rename to doc/release/prev_whats_new/github_stats_3.0.3.rst index 5c1271e52e4f..a70c83ecfec8 100644 --- a/doc/users/prev_whats_new/github_stats_3.0.3.rst +++ b/doc/release/prev_whats_new/github_stats_3.0.3.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.0.3 + .. _github-stats-3-0-3: GitHub statistics for 3.0.3 (Feb 28, 2019) diff --git a/doc/users/prev_whats_new/github_stats_3.1.0.rst b/doc/release/prev_whats_new/github_stats_3.1.0.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.1.0.rst rename to doc/release/prev_whats_new/github_stats_3.1.0.rst index a0fb2692fdbb..fe553a4af8f3 100644 --- a/doc/users/prev_whats_new/github_stats_3.1.0.rst +++ b/doc/release/prev_whats_new/github_stats_3.1.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.1.0 + .. _github-stats-3-1-0: GitHub statistics for 3.1.0 (May 18, 2019) diff --git a/doc/users/prev_whats_new/github_stats_3.1.1.rst b/doc/release/prev_whats_new/github_stats_3.1.1.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.1.1.rst rename to doc/release/prev_whats_new/github_stats_3.1.1.rst index 3e552c371c55..a84fe93d6808 100644 --- a/doc/users/prev_whats_new/github_stats_3.1.1.rst +++ b/doc/release/prev_whats_new/github_stats_3.1.1.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.1.1 + .. _github-stats-3-1-1: GitHub statistics for 3.1.1 (Jul 02, 2019) diff --git a/doc/users/prev_whats_new/github_stats_3.1.2.rst b/doc/release/prev_whats_new/github_stats_3.1.2.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.1.2.rst rename to doc/release/prev_whats_new/github_stats_3.1.2.rst index d8476cb5c3a8..c966c2fbcaf0 100644 --- a/doc/users/prev_whats_new/github_stats_3.1.2.rst +++ b/doc/release/prev_whats_new/github_stats_3.1.2.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.1.2 + .. _github-stats-3-1-2: GitHub statistics for 3.1.2 (Nov 21, 2019) diff --git a/doc/users/prev_whats_new/github_stats_3.1.3.rst b/doc/release/prev_whats_new/github_stats_3.1.3.rst similarity index 98% rename from doc/users/prev_whats_new/github_stats_3.1.3.rst rename to doc/release/prev_whats_new/github_stats_3.1.3.rst index f8c1afb0e177..604606e98a42 100644 --- a/doc/users/prev_whats_new/github_stats_3.1.3.rst +++ b/doc/release/prev_whats_new/github_stats_3.1.3.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.1.3 + .. _github-stats-3-1-3: GitHub statistics for 3.1.3 (Feb 03, 2020) diff --git a/doc/users/prev_whats_new/github_stats_3.10.0.rst b/doc/release/prev_whats_new/github_stats_3.10.0.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.10.0.rst rename to doc/release/prev_whats_new/github_stats_3.10.0.rst index 01b54708b7ec..d61150e6bd6a 100644 --- a/doc/users/prev_whats_new/github_stats_3.10.0.rst +++ b/doc/release/prev_whats_new/github_stats_3.10.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.10.0 + .. _github-stats-3_10_0: GitHub statistics for 3.10.0 (Dec 13, 2024) diff --git a/doc/users/github_stats.rst b/doc/release/prev_whats_new/github_stats_3.10.1.rst similarity index 98% rename from doc/users/github_stats.rst rename to doc/release/prev_whats_new/github_stats_3.10.1.rst index de1f85004f09..c7fac6d5c61d 100644 --- a/doc/users/github_stats.rst +++ b/doc/release/prev_whats_new/github_stats_3.10.1.rst @@ -1,4 +1,4 @@ -.. _github-stats: +.. _github-stats-3_10_1: GitHub statistics for 3.10.1 (Feb 27, 2025) =========================================== @@ -169,14 +169,3 @@ Issues (14): * :ghissue:`25274`: [Bug]: .remove() on ErrorbarContainer object does not remove the corresponding item from the legend * :ghissue:`29202`: [Bug]: ``fontsize`` in tables not working * :ghissue:`29301`: [Bug]: Blank EPS output with legend and annotate - - -Previous GitHub statistics --------------------------- - -.. toctree:: - :maxdepth: 1 - :glob: - :reversed: - - prev_whats_new/github_stats_* diff --git a/doc/release/prev_whats_new/github_stats_3.10.3.rst b/doc/release/prev_whats_new/github_stats_3.10.3.rst new file mode 100644 index 000000000000..3e1d350dae5c --- /dev/null +++ b/doc/release/prev_whats_new/github_stats_3.10.3.rst @@ -0,0 +1,144 @@ +.. _github-stats_3-10-3: + +GitHub statistics for 3.10.3 (May 08, 2025) +=========================================== + +GitHub statistics for 2025/02/27 (tag: v3.10.1) - 2025/05/08 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 16 issues and merged 78 pull requests. +The full list can be seen `on GitHub `__ + +The following 28 authors contributed 128 commits. + +* Alexandra Khoo +* Antony Lee +* Carlos Ramos Carreño +* David Lowry-Duda +* David Stansby +* DerWeh +* Elliott Sales de Andrade +* guillermodotn +* hannah +* Hassan Kibirige +* Ian Thomas +* James Addison +* Jody Klymak +* Kyle Sunden +* Marten H. van Kerkwijk +* Marten Henric van Kerkwijk +* martincornejo +* Mateusz Sokół +* Nicolai Weitkemper +* Oscar Gustafsson +* Praful Gulani +* prafulgulani555 +* Qian Zhang +* Raphael Erik Hviding +* Ruth Comer +* Thomas A Caswell +* Tim Hoffmann +* Weh Andreas + +GitHub issues and pull requests: + +Pull Requests (78): + +* :ghpull:`30018`: Backport PR #29907 on branch v3.10.x (Ensure text metric calculation always uses the text cache) +* :ghpull:`30010`: Backport PR #29992 on v3.10.x: Update pinned oldest win image on azure +* :ghpull:`29992`: Update pinned oldest win image on azure +* :ghpull:`29867`: Backport PR #29827 on branch v3.10.x (TST: Remove unnecessary test images) +* :ghpull:`30002`: Backport PR #29673 on branch v3.10.x (DOC: document the issues with overlaying new mpl on old mpl) +* :ghpull:`29673`: DOC: document the issues with overlaying new mpl on old mpl +* :ghpull:`29999`: Backport PR #29997 on branch v3.10.x (BLD: Ensure meson.build has the right version of Python) +* :ghpull:`29997`: BLD: Ensure meson.build has the right version of Python +* :ghpull:`29996`: Backport PR #29995 on branch v3.10.x (Fix typo: missing singlequote in unrecognized backend exception) +* :ghpull:`29995`: Fix typo: missing singlequote in unrecognized backend exception +* :ghpull:`29990`: Backport PR #29789 on branch v3.10.x (Improve layout of cheatsheets in sidebar) +* :ghpull:`29987`: Backport PR #29370 on branch v3.10.x (DOC: Improve NonUniformImage docs) +* :ghpull:`29370`: DOC: Improve NonUniformImage docs +* :ghpull:`29983`: Backport PR #29975 on branch v3.10.x (DOC: correct signature for animation update function in rain example) +* :ghpull:`29974`: Backport PR #29970 on branch v3.10.x (TST: Make refcount tests more resilient to Python changes) +* :ghpull:`29975`: DOC: correct signature for animation update function in rain example +* :ghpull:`29980`: Backport PR #29979 on branch v3.10.x (Fix typos: horizonatal -> horizontal) +* :ghpull:`29979`: Fix typos: horizonatal -> horizontal +* :ghpull:`29970`: TST: Make refcount tests more resilient to Python changes +* :ghpull:`29969`: Backport PR #29965 on branch v3.10.x (Document Axes.spines) +* :ghpull:`29965`: Document Axes.spines +* :ghpull:`29949`: Backport PR #29796 on branch v3.10.x: ci: rotate soon-to-be-unsupported GitHub Actions ubuntu-20.04 runner out of roster +* :ghpull:`29901`: Backport PR #29872 on branch v3.10.x (TST: Use placeholders for text in layout tests) +* :ghpull:`29933`: Backport PR #29931 on branch v3.10.x (Allow Python native sequences in Matplotlib ``imsave()``.) +* :ghpull:`29943`: Fix doc build on 3.10.x +* :ghpull:`29940`: Backport PR #29919 on branch v3.10.x (Handle MOVETO's, CLOSEPOLY's and empty paths in Path.interpolated) +* :ghpull:`29919`: Handle MOVETO's, CLOSEPOLY's and empty paths in Path.interpolated +* :ghpull:`29908`: TST: Use text placeholders for empty legends +* :ghpull:`29931`: Allow Python native sequences in Matplotlib ``imsave()``. +* :ghpull:`29932`: Backport PR #29920 on branch v3.10.x (Allow ``None`` in set_prop_cycle (in type hints)) +* :ghpull:`29920`: Allow ``None`` in set_prop_cycle (in type hints) +* :ghpull:`29927`: Backport PR #29897 on branch v3.10.x (BUG: ensure that errorbar does not error on masked negative errors.) +* :ghpull:`29930`: Backport PR #29929 on branch v3.10.x (Correct rightparen typo) +* :ghpull:`29929`: Correct rightparen typo +* :ghpull:`29897`: BUG: ensure that errorbar does not error on masked negative errors. +* :ghpull:`29907`: Ensure text metric calculation always uses the text cache +* :ghpull:`29902`: Backport PR #29899 on branch v3.10.x ([doc] minimally document what basic units is doing) +* :ghpull:`29900`: Backport PR #29896 on branch v3.10.x (Change ``.T`` to ``.transpose()`` in ``_reshape_2D``) +* :ghpull:`29872`: TST: Use placeholders for text in layout tests +* :ghpull:`29896`: Change ``.T`` to ``.transpose()`` in ``_reshape_2D`` +* :ghpull:`29888`: Backport PR #29803 on branch v3.10.x (DOC: Improve FancyArrowPatch docstring) +* :ghpull:`29880`: Backport PR #29853 on branch v3.10.x (Update lib/matplotlib/stackplot.py) +* :ghpull:`29853`: Update lib/matplotlib/stackplot.py +* :ghpull:`29868`: Backport PR #29834 on branch v3.10.x (TST: pin flake8) +* :ghpull:`29827`: TST: Remove unnecessary test images +* :ghpull:`29861`: Backport PR #29773 on branch v3.10.x (DOC: Improve interactive figures guide / Blocking input) +* :ghpull:`29859`: Backport PR #29545 on branch v3.10.x (DOC: correctly specify return type of ``figaspect``) +* :ghpull:`29545`: DOC: correctly specify return type of ``figaspect`` +* :ghpull:`29858`: Backport PR #29842 on branch v3.10.x (Don't drag draggables on scroll events) +* :ghpull:`29842`: Don't drag draggables on scroll events +* :ghpull:`29848`: Backport PR #29839 on branch v3.10.x (Improve docs regarding plt.close().) +* :ghpull:`29839`: Improve docs regarding plt.close(). +* :ghpull:`29818`: Backport PR #29801 on branch v3.10.x (DOC: Slightly further improve arrowstyle demo) +* :ghpull:`29814`: Backport PR #29552 on branch v3.10.x (Bug Fix: Normalize kwargs for Histogram) +* :ghpull:`29792`: Backport PR #29770 on branch v3.10.x (MNT: Move test for old ipython behavior to minver tests) +* :ghpull:`29750`: Backport PR #29748 on branch v3.10.x (Fix PyGObject version pinning in macOS tests) +* :ghpull:`29754`: Backport PR #29721 on branch v3.10.x (FIX: pyplot auto-backend detection case-sensitivity fixup) +* :ghpull:`29786`: Backport PR #29755 on branch v3.10.x (DOC: Simplify annotation arrow style reference) +* :ghpull:`29784`: Backport PR #29781 on branch v3.10.x (Fix escaping of nulls and "0" in default filenames.) +* :ghpull:`29781`: Fix escaping of nulls and "0" in default filenames. +* :ghpull:`29771`: Backport PR #29752 on branch v3.10.x (DOC: Add install instructions for pixi and uv) +* :ghpull:`29768`: Backport PR #29767 on branch v3.10.x (Add description to logit_demo.py script) +* :ghpull:`29721`: FIX: pyplot auto-backend detection case-sensitivity fixup +* :ghpull:`29737`: Backport PR #29734 on branch v3.10.x (ci: MacOS 14: temporarily upper-bound the 'PyGObject' Python package version) +* :ghpull:`29735`: Backport PR #29719 on branch v3.10.x (Fix passing singleton sequence-type styles to hist) +* :ghpull:`29719`: Fix passing singleton sequence-type styles to hist +* :ghpull:`29730`: Backport PR #29724 on branch v3.10.x (Fix SubplotSpec.get_gridspec type hint) +* :ghpull:`29724`: Fix SubplotSpec.get_gridspec type hint +* :ghpull:`29727`: Backport PR #29726 on branch v3.10.x (Add reference tag to Hatch style reference) +* :ghpull:`29709`: Backport PR #29708 on branch v3.10.x (MNT: correct version in plotting method deprecation warnings) +* :ghpull:`29708`: MNT: correct version in plotting method deprecation warnings +* :ghpull:`29692`: Backport PR #29689 on branch v3.10.x (Fix alt and caption handling in Sphinx directives) +* :ghpull:`29693`: Backport PR #29590 on branch v3.10.x (Blocked set_clim() callbacks to prevent inconsistent state (#29522)) +* :ghpull:`29590`: Blocked set_clim() callbacks to prevent inconsistent state (#29522) +* :ghpull:`29689`: Fix alt and caption handling in Sphinx directives +* :ghpull:`29691`: Backport PR #29584 on branch v3.10.x (DOC: Recommend constrained_layout over tight_layout) +* :ghpull:`29584`: DOC: Recommend constrained_layout over tight_layout +* :ghpull:`29552`: Bug Fix: Normalize kwargs for Histogram + +Issues (16): + +* :ghissue:`29183`: [Bug]: I give an RGB image to imsave but I don't have the right color map! +* :ghissue:`29797`: [MNT]: Flaky Windows_py31x tests on Azure Pipelines +* :ghissue:`26827`: [Bug]: ImportError when using Matplotlib v3.8.0 in Python package tests +* :ghissue:`29964`: [Doc]: object description for "spines"of matplot.axes.Axes not found +* :ghissue:`29917`: [Bug]: Contour plots using mollweide-projection +* :ghissue:`29540`: [Bug]: matshow(..., fignum=...) broken +* :ghissue:`29142`: [MNT]: Draggable legend gets stuck on cursor after scroll event +* :ghissue:`29857`: [Bug]: Unexpected behavior of the line style specifiers in the histogram method +* :ghissue:`29766`: [MNT]: ci: ubuntu-20.04 GitHub Actions runner will soon be unmaintained +* :ghissue:`29812`: [MNT]: Backport request for #29552 to 3.10.x +* :ghissue:`29779`: [Bug]: get_default_filename removes '0' from file name instead of '\0' from window title +* :ghissue:`29713`: [Bug]: Matplotlib selects TkAgg backend on LXC containers +* :ghissue:`29717`: [Bug]: Using a linestyle tuple with a histogram crashes with matplotlib 3.10 +* :ghissue:`29522`: [Bug]: Image color limits not correctly updated with set_clim() IFF color bar present AND new norm.vmin > old norm.vmax +* :ghissue:`17339`: Clarify that constrained_layout and tight_layout conflict with each other +* :ghissue:`28884`: [ENH]: Expand ``hist()`` signature to support aliases and plural kwargs diff --git a/doc/release/prev_whats_new/github_stats_3.10.5.rst b/doc/release/prev_whats_new/github_stats_3.10.5.rst new file mode 100644 index 000000000000..319086baebe5 --- /dev/null +++ b/doc/release/prev_whats_new/github_stats_3.10.5.rst @@ -0,0 +1,142 @@ +.. _github-stats-3_10_5: + +GitHub statistics for 3.10.5 (Jul 31, 2025) +=========================================== + +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2025/07/31 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 18 issues and merged 67 pull requests. +The full list can be seen `on GitHub `__ + +The following 36 authors contributed 371 commits. + +* Antony Lee +* Brian Christian +* chrisjbillington +* Christine P. Chai +* Clément Robert +* David Stansby +* dependabot[bot] +* Elliott Sales de Andrade +* G.D. McBain +* Greg Lucas +* hannah +* hu-xiaonan +* Ian Thomas +* ianlv +* IdiotCoffee +* Ines Cachola +* Inês Cachola +* Jody Klymak +* Jouni K. Seppänen +* Khushi_29 +* Kyle Sunden +* Lumberbot (aka Jack) +* N R Navaneet +* Nathan G. Wiseman +* Oscar Gustafsson +* Praful Gulani +* Qian Zhang +* Raphael Erik Hviding +* Roman +* Roman A +* Ruth Comer +* saikarna913 +* Scott Shambaugh +* Thomas A Caswell +* Tim Hoffmann +* Trygve Magnus Ræder + +GitHub issues and pull requests: + +Pull Requests (67): + +* :ghpull:`30357`: CIBW updates: fix pypy sections, update cibw version +* :ghpull:`30356`: Manual Backport PR #30195 on branch v3.10.x (ci: Enable wheel builds on Python 3.14) +* :ghpull:`30352`: Backport PR #28554 on branch v3.10.x (BLD: Enable wheels on Windows-on-ARM) +* :ghpull:`30353`: Backport PR #30345 on branch v3.10.x (qt: Use better devicePixelRatio event to refresh scaling) +* :ghpull:`30350`: Backport PR #30344 on branch v3.10.x (Support fractional HiDPI in GTK4 backend) +* :ghpull:`30277`: Backport PR #30271 on branch v3.10.x (Reduce pause time in interactive timer test) +* :ghpull:`30351`: Backport PR #30327 on branch v3.10.x (FIX Update Axes limits from Axes.add_collection(... autolim=True)) +* :ghpull:`30345`: qt: Use better devicePixelRatio event to refresh scaling +* :ghpull:`28554`: BLD: Enable wheels on Windows-on-ARM +* :ghpull:`30292`: Backport PR #30237: Add explicit ``**options: Any`` for ``add_subplot`` m… +* :ghpull:`29935`: Backport PR #29908 on branch v3.10.x (TST: Use text placeholders for empty legends) +* :ghpull:`30327`: FIX Update Axes limits from Axes.add_collection(... autolim=True) +* :ghpull:`30344`: Support fractional HiDPI in GTK4 backend +* :ghpull:`30326`: Backport PR #30321 on branch v3.10.x (Fix type annotation for Axes.get_legend() to include None) +* :ghpull:`30321`: Fix type annotation for Axes.get_legend() to include None +* :ghpull:`30287`: Backport PR #30286 on branch v3.10.x (Fix whitespace in _axes.py error message) +* :ghpull:`30288`: Backport PR #30283 on branch v3.10.x (changed the FAQ link to point to the correct path) +* :ghpull:`30293`: Backport PR #30289 on branch v3.10.x (DOC: Fix build with pybind11 3) +* :ghpull:`30283`: changed the FAQ link to point to the correct path +* :ghpull:`30286`: Fix whitespace in _axes.py error message +* :ghpull:`30271`: Reduce pause time in interactive timer test +* :ghpull:`30269`: Backport PR #30186 on branch v3.10.x (Fix figure legend when drawing stackplots) +* :ghpull:`30186`: Fix figure legend when drawing stackplots +* :ghpull:`30268`: Backport PR #30233 on branch v3.10.x (Check that stem input is 1D) +* :ghpull:`30233`: Check that stem input is 1D +* :ghpull:`30259`: Backport PR #30256 on branch v3.10.x (Time out in _get_executable_info) +* :ghpull:`30256`: Time out in _get_executable_info +* :ghpull:`30237`: Add explicit ``**options: Any`` for ``add_subplot`` method +* :ghpull:`30253`: Backport PR #30243 on branch v3.10.x (Fix FancyArrow rendering for zero-length arrows) +* :ghpull:`30243`: Fix FancyArrow rendering for zero-length arrows +* :ghpull:`30250`: Backport PR #30244 on branch v3.10.x (DOC: Recommend to use bare Figure instances for saving to file) +* :ghpull:`30247`: Backport PR #30246 on branch v3.10.x (chore: remove redundant words in comment) +* :ghpull:`30246`: chore: remove redundant words in comment +* :ghpull:`30240`: Backport PR #30236 on branch v3.10.x (Copy-edit the docstring of AuxTransformBox.) +* :ghpull:`30236`: Copy-edit the docstring of AuxTransformBox. +* :ghpull:`30234`: Backport PR #30209 on branch v3.10.x (Clean up Qt socket notifier to avoid spurious interrupt handler calls) +* :ghpull:`30209`: Clean up Qt socket notifier to avoid spurious interrupt handler calls +* :ghpull:`30195`: ci: Enable wheel builds on Python 3.14 +* :ghpull:`30229`: Backport PR #30221 on branch v3.10.x (BUG: fix future incompatibility with Pillow 13) +* :ghpull:`30221`: BUG: fix future incompatibility with Pillow 13 +* :ghpull:`30228`: Backport PR #30098 on branch v3.10.x (Fix label_outer in the presence of colorbars.) +* :ghpull:`30227`: Backport PR #30223 on branch v3.10.x (Polar log scale: fix inner patch boundary and spine location) +* :ghpull:`30098`: Fix label_outer in the presence of colorbars. +* :ghpull:`30223`: Polar log scale: fix inner patch boundary and spine location +* :ghpull:`30217`: Backport PR #30198 on branch v3.10.x (Implement Path.__deepcopy__ avoiding infinite recursion) +* :ghpull:`30198`: Implement Path.__deepcopy__ avoiding infinite recursion +* :ghpull:`30213`: Backport PR #30212 on branch v3.10.x ([Doc]: fix bug in release notes for matplotlib v3.5.0 and v3.7.0) +* :ghpull:`30189`: Backport PR #30180 on branch v3.10.x (DOC: expand polar example) +* :ghpull:`30167`: Backport PR #30162 on branch v3.10.x (TST: Fix runtime error checking NaN input to format_cursor_data) +* :ghpull:`30162`: TST: Fix runtime error checking NaN input to format_cursor_data +* :ghpull:`30146`: Backport PR #30144 on branch v3.10.x (js: Fix externally-controlled format strings) +* :ghpull:`30144`: js: Fix externally-controlled format strings +* :ghpull:`30140`: Backport PR #30118 on branch v3.10.x (CI: Skip jobs on forks) +* :ghpull:`30120`: Backport PR #30114 on branch v3.10.x (Fix _is_tensorflow_array.) +* :ghpull:`30122`: Backport PR #30119 on branch v3.10.x (Add some types to _mathtext.py) +* :ghpull:`30119`: Add some types to _mathtext.py +* :ghpull:`30114`: Fix _is_tensorflow_array. +* :ghpull:`30106`: Backport PR #30089 on branch v3.10.x (FIX: fix submerged margins algorithm being applied twice) +* :ghpull:`30089`: FIX: fix submerged margins algorithm being applied twice +* :ghpull:`30101`: Backport PR #30096 on branch v3.10.x (Fix OffsetBox custom picker) +* :ghpull:`30096`: Fix OffsetBox custom picker +* :ghpull:`30081`: Backport PR #30079 on branch v3.10.x (FIX: cast legend handles to list) +* :ghpull:`30079`: FIX: cast legend handles to list +* :ghpull:`30057`: Backport PR #29895 on branch v3.10.x (The 'lines.markeredgecolor' now doesn't interfere on the color of errorbar caps)" +* :ghpull:`29895`: The 'lines.markeredgecolor' now doesn't interfere on the color of errorbar caps +* :ghpull:`30033`: Backport PR #30029 on branch v3.10.x (Update diagram in subplots_adjust documentation to clarify parameters) + +Issues (18): + +* :ghissue:`30370`: [Bug]: matplotlib simple example fails in Python 3.14 +* :ghissue:`30218`: [Bug]: Rendering on Wayland with fractional scaling looks bad +* :ghissue:`30318`: [Bug]: type annotation of ``Axes.get_legend()`` misses ``None`` +* :ghissue:`30169`: [Doc]: Incorrect FAQ Link on Tutorials Page +* :ghissue:`30285`: [Bug]: Missing whitespace in _axes.py error message +* :ghissue:`30280`: [Bug]: Pillow 11.3 raises a deprecation warning when using TkAgg +* :ghissue:`30158`: [Bug]: Stackplot in SubFigure raises when drawing Legend +* :ghissue:`30216`: [Bug]: stem complaining about PyTorch's Tensor +* :ghissue:`30242`: [Bug]: Cannot create empty FancyArrow (expired numpy deprecation) +* :ghissue:`30249`: [Bug]: DeprecationWarning from Pillow 11.3.0 about 'mode' parameter of PIL.Image.fromarray() +* :ghissue:`29688`: [Bug]: "Bad file descriptor" raised repeatedly when plt.pause() interrupted in IPython +* :ghissue:`27305`: [Bug]: Axes.label_outer() does not work when there is a colorbar +* :ghissue:`30179`: [Bug]: Inner border is not rendered correctly when using log-scale and polar projection. +* :ghissue:`29157`: FUTURE BUG: reconsider how we deep-copy path objects +* :ghissue:`30152`: [Bug]: Test pipeline failure on windows +* :ghissue:`30076`: [Bug]: Layout Managers are confused by complex arrangement of sub-figures and gridspec's +* :ghissue:`30078`: [Bug]: legend no longer works with itertools.chain +* :ghissue:`29780`: [Bug]: Setting 'lines.markeredgecolor' affects color of errorbar caps. diff --git a/doc/release/prev_whats_new/github_stats_3.10.6.rst b/doc/release/prev_whats_new/github_stats_3.10.6.rst new file mode 100644 index 000000000000..fb88af0ae10f --- /dev/null +++ b/doc/release/prev_whats_new/github_stats_3.10.6.rst @@ -0,0 +1,76 @@ +.. _github-stats-3_10_6: + +GitHub statistics for 3.10.6 (Aug 29, 2025) +=========================================== + +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2025/08/29 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 4 issues and merged 19 pull requests. +The full list can be seen `on GitHub `__ + +The following 31 authors contributed 380 commits. + +* Alan Burlot +* Antony Lee +* Christine P. Chai +* David Stansby +* dependabot[bot] +* Doron Behar +* Elliott Sales de Andrade +* G.D. McBain +* Greg Lucas +* hannah +* hu-xiaonan +* Ian Thomas +* Inês Cachola +* Jody Klymak +* Jouni K. Seppänen +* Khushi_29 +* Kyle Sunden +* Lumberbot (aka Jack) +* N R Navaneet +* Nathan G. Wiseman +* Oscar Gustafsson +* Praful Gulani +* Qian Zhang +* Raphael Erik Hviding +* Roman +* Ruth Comer +* saikarna913 +* Scott Shambaugh +* Thomas A Caswell +* Tim Hoffmann +* Trygve Magnus Ræder + +GitHub issues and pull requests: + +Pull Requests (19): + +* :ghpull:`30487`: Backport PR #30484 on branch v3.10.x (FIX: be more cautious about checking widget size) +* :ghpull:`30484`: FIX: be more cautious about checking widget size +* :ghpull:`30481`: Backport PR #30394 on branch v3.10.x (ENH: Gracefully handle python-build-standalone ImportError with Tk) +* :ghpull:`30477`: Backport PR #30476 on branch v3.10.x (ci: Remove cibuildwheel override for win_arm64/Py3.14) +* :ghpull:`30394`: ENH: Gracefully handle python-build-standalone ImportError with Tk +* :ghpull:`30476`: ci: Remove cibuildwheel override for win_arm64/Py3.14 +* :ghpull:`30461`: Backport PR #30451 on branch v3.10.x (doc: factor out quick install tab for reuse) +* :ghpull:`30448`: Backport PR #30412 on branch v3.10.x ({Check,Radio}Buttons: Improve docs of label_props) +* :ghpull:`30412`: {Check,Radio}Buttons: Improve docs of label_props +* :ghpull:`30445`: Backport PR #30444 on branch v3.10.x (Small correction of a typo in the galleries: axis instead of axes) +* :ghpull:`30444`: Small correction of a typo in the galleries: axis instead of axes +* :ghpull:`30430`: Backport PR #30426 on branch v3.10.x (Fix a race condition in TexManager.make_dvi.) +* :ghpull:`30434`: Backport PR #30426: Fix a race condition in TexManager.make_dvi & make_png. +* :ghpull:`30431`: Use pathlib in texmanager. +* :ghpull:`30428`: Backport PR #30399 on branch v3.10.x (Qt: Fix HiDPI handling on X11/Windows) +* :ghpull:`30426`: Fix a race condition in TexManager.make_dvi. +* :ghpull:`30399`: Qt: Fix HiDPI handling on X11/Windows +* :ghpull:`30415`: Backport PR #30414 on branch v3.10.x (DOC: update Cartopy url) +* :ghpull:`30414`: DOC: update Cartopy url + +Issues (4): + +* :ghissue:`29618`: [Bug]: FigureCanvasQT is seemingly prematurely freed under certain conditions +* :ghissue:`30390`: [ENH]: Gracefully handle python-build-standalone ImportError +* :ghissue:`30420`: [ENH]: Support parallel plotting +* :ghissue:`30386`: BUG: Qt hi-dpi regression on windows and X11 with mpl 3.10.5 diff --git a/doc/release/prev_whats_new/github_stats_3.10.7.rst b/doc/release/prev_whats_new/github_stats_3.10.7.rst new file mode 100644 index 000000000000..8c12e4050219 --- /dev/null +++ b/doc/release/prev_whats_new/github_stats_3.10.7.rst @@ -0,0 +1,74 @@ +.. _github-stats_3-10-7: + +GitHub statistics for 3.10.7 (Oct 08, 2025) +=========================================== + +GitHub statistics for 2024/12/14 (tag: v3.10.0) - 2025/10/08 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 4 issues and merged 16 pull requests. +The full list can be seen `on GitHub `__ + +The following 32 authors contributed 422 commits. + +* Aasma Gupta +* AASMA GUPTA +* Antony Lee +* Christine P. Chai +* David Stansby +* dependabot[bot] +* Elliott Sales de Andrade +* G.D. McBain +* Greg Lucas +* hannah +* hu-xiaonan +* Ian Thomas +* Inês Cachola +* Jody Klymak +* Jouni K. Seppänen +* Khushi_29 +* Kyle Sunden +* Lumberbot (aka Jack) +* N R Navaneet +* Nathan G. Wiseman +* Oscar Gustafsson +* Praful Gulani +* Qian Zhang +* Raphael Erik Hviding +* Roman +* Ruth Comer +* saikarna913 +* Scott Shambaugh +* Thomas A Caswell +* Tim Heap +* Tim Hoffmann +* Trygve Magnus Ræder + +GitHub issues and pull requests: + +Pull Requests (16): + +* :ghpull:`30628`: Backport PR #30626 on branch v3.10.x (MNT: Fix new F401 unused imports warnings) +* :ghpull:`30626`: MNT: Fix new F401 unused imports warnings +* :ghpull:`30589`: Backport PR #29745: Use PEP8 style method and function names from +* :ghpull:`30614`: Backport PR #30612 on branch v3.10.x (MNT: update black pin) +* :ghpull:`30612`: MNT: update black pin +* :ghpull:`30572`: Backport PR #30571 on branch v3.10.x (CI: remove macos13) +* :ghpull:`30571`: CI: remove macos13 +* :ghpull:`30570`: Backport PR #30558 on branch v3.10.x (Fix stubtest with mypy 18) +* :ghpull:`30558`: Fix stubtest with mypy 18 +* :ghpull:`30540`: Backport PR #30539 on branch v3.10.x (Fix scale_unit/scale_units typo in quiver docs) +* :ghpull:`30539`: Fix scale_unit/scale_units typo in quiver docs +* :ghpull:`30518`: Backport PR #30497 on branch v3.10.x (TST: Use a temporary directory for test_save_figure_return) +* :ghpull:`30497`: TST: Use a temporary directory for test_save_figure_return +* :ghpull:`30506`: Backport PR #30490 on branch v3.10.x (Fix SVG rendering error in def update_background) +* :ghpull:`30490`: Fix SVG rendering error in def update_background +* :ghpull:`30494`: Backport PR #30492 on branch v3.10.x (DOC: pytz link should be from PyPI) + +Issues (4): + +* :ghissue:`30611`: [MNT]: black version +* :ghissue:`30551`: [Bug]: Mypy stubtest failure on disjoint_base +* :ghissue:`30493`: [Bug]: test_save_figure_return seems flaky +* :ghissue:`30485`: [Bug]: figures with SpanSelector(..., useblit=True) can't be saved to SVG or PDF diff --git a/doc/users/prev_whats_new/github_stats_3.2.0.rst b/doc/release/prev_whats_new/github_stats_3.2.0.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.2.0.rst rename to doc/release/prev_whats_new/github_stats_3.2.0.rst index 4efdb191494d..32151f0898a8 100644 --- a/doc/users/prev_whats_new/github_stats_3.2.0.rst +++ b/doc/release/prev_whats_new/github_stats_3.2.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.2.0 + .. _github-stats-3-2-0: GitHub statistics for 3.2.0 (Mar 04, 2020) diff --git a/doc/users/prev_whats_new/github_stats_3.2.1.rst b/doc/release/prev_whats_new/github_stats_3.2.1.rst similarity index 98% rename from doc/users/prev_whats_new/github_stats_3.2.1.rst rename to doc/release/prev_whats_new/github_stats_3.2.1.rst index 4f865dbb5429..a6b2eb1bfe55 100644 --- a/doc/users/prev_whats_new/github_stats_3.2.1.rst +++ b/doc/release/prev_whats_new/github_stats_3.2.1.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.2.1 + .. _github-stats-3-2-1: GitHub statistics for 3.2.1 (Mar 18, 2020) diff --git a/doc/users/prev_whats_new/github_stats_3.2.2.rst b/doc/release/prev_whats_new/github_stats_3.2.2.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.2.2.rst rename to doc/release/prev_whats_new/github_stats_3.2.2.rst index 9026d518ce4d..d6aae86d9b43 100644 --- a/doc/users/prev_whats_new/github_stats_3.2.2.rst +++ b/doc/release/prev_whats_new/github_stats_3.2.2.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.2.2 + .. _github-stats-3-2-2: GitHub statistics for 3.2.2 (Jun 17, 2020) diff --git a/doc/users/prev_whats_new/github_stats_3.3.0.rst b/doc/release/prev_whats_new/github_stats_3.3.0.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.3.0.rst rename to doc/release/prev_whats_new/github_stats_3.3.0.rst index 45813659b890..47be96d0a2cb 100644 --- a/doc/users/prev_whats_new/github_stats_3.3.0.rst +++ b/doc/release/prev_whats_new/github_stats_3.3.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.3.0 + .. _github-stats-3-3-0: GitHub statistics for 3.3.0 (Jul 16, 2020) diff --git a/doc/users/prev_whats_new/github_stats_3.3.1.rst b/doc/release/prev_whats_new/github_stats_3.3.1.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.3.1.rst rename to doc/release/prev_whats_new/github_stats_3.3.1.rst index dc8e9996313f..24df6db38776 100644 --- a/doc/users/prev_whats_new/github_stats_3.3.1.rst +++ b/doc/release/prev_whats_new/github_stats_3.3.1.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.3.1 + .. _github-stats-3-3-1: GitHub statistics for 3.3.1 (Aug 13, 2020) diff --git a/doc/users/prev_whats_new/github_stats_3.3.2.rst b/doc/release/prev_whats_new/github_stats_3.3.2.rst similarity index 98% rename from doc/users/prev_whats_new/github_stats_3.3.2.rst rename to doc/release/prev_whats_new/github_stats_3.3.2.rst index 0bc03cbc83ee..8875a51c830f 100644 --- a/doc/users/prev_whats_new/github_stats_3.3.2.rst +++ b/doc/release/prev_whats_new/github_stats_3.3.2.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.3.2 + .. _github-stats-3-3-2: GitHub statistics for 3.3.2 (Sep 15, 2020) diff --git a/doc/users/prev_whats_new/github_stats_3.3.3.rst b/doc/release/prev_whats_new/github_stats_3.3.3.rst similarity index 98% rename from doc/users/prev_whats_new/github_stats_3.3.3.rst rename to doc/release/prev_whats_new/github_stats_3.3.3.rst index 5475a5209eed..dc8f9964c30f 100644 --- a/doc/users/prev_whats_new/github_stats_3.3.3.rst +++ b/doc/release/prev_whats_new/github_stats_3.3.3.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.3.3 + .. _github-stats-3-3-3: GitHub statistics for 3.3.3 (Nov 11, 2020) diff --git a/doc/users/prev_whats_new/github_stats_3.3.4.rst b/doc/release/prev_whats_new/github_stats_3.3.4.rst similarity index 97% rename from doc/users/prev_whats_new/github_stats_3.3.4.rst rename to doc/release/prev_whats_new/github_stats_3.3.4.rst index afff8b384b8e..23fcf6fe0da3 100644 --- a/doc/users/prev_whats_new/github_stats_3.3.4.rst +++ b/doc/release/prev_whats_new/github_stats_3.3.4.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.3.4 + .. _github-stats-3-3-4: GitHub statistics for 3.3.4 (Jan 28, 2021) diff --git a/doc/users/prev_whats_new/github_stats_3.4.0.rst b/doc/release/prev_whats_new/github_stats_3.4.0.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.4.0.rst rename to doc/release/prev_whats_new/github_stats_3.4.0.rst index fb6f0044d139..c6cc8f7091d6 100644 --- a/doc/users/prev_whats_new/github_stats_3.4.0.rst +++ b/doc/release/prev_whats_new/github_stats_3.4.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.4.0 + .. _github-stats-3-4-0: GitHub statistics for 3.4.0 (Mar 26, 2021) diff --git a/doc/users/prev_whats_new/github_stats_3.4.1.rst b/doc/release/prev_whats_new/github_stats_3.4.1.rst similarity index 97% rename from doc/users/prev_whats_new/github_stats_3.4.1.rst rename to doc/release/prev_whats_new/github_stats_3.4.1.rst index 0819a6850a3e..5452bfd15349 100644 --- a/doc/users/prev_whats_new/github_stats_3.4.1.rst +++ b/doc/release/prev_whats_new/github_stats_3.4.1.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.4.1 + .. _github-stats-3-4-1: GitHub statistics for 3.4.1 (Mar 31, 2021) diff --git a/doc/users/prev_whats_new/github_stats_3.4.2.rst b/doc/release/prev_whats_new/github_stats_3.4.2.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.4.2.rst rename to doc/release/prev_whats_new/github_stats_3.4.2.rst index d16a69b43151..4d5e13e9a587 100644 --- a/doc/users/prev_whats_new/github_stats_3.4.2.rst +++ b/doc/release/prev_whats_new/github_stats_3.4.2.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.4.2 + .. _github-stats-3-4-2: GitHub statistics for 3.4.2 (May 08, 2021) diff --git a/doc/users/prev_whats_new/github_stats_3.4.3.rst b/doc/release/prev_whats_new/github_stats_3.4.3.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.4.3.rst rename to doc/release/prev_whats_new/github_stats_3.4.3.rst index ff98041e2d72..9256b20b1e16 100644 --- a/doc/users/prev_whats_new/github_stats_3.4.3.rst +++ b/doc/release/prev_whats_new/github_stats_3.4.3.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.4.3 + .. _github-stats-3-4-3: GitHub statistics for 3.4.3 (August 21, 2021) diff --git a/doc/users/prev_whats_new/github_stats_3.5.0.rst b/doc/release/prev_whats_new/github_stats_3.5.0.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.5.0.rst rename to doc/release/prev_whats_new/github_stats_3.5.0.rst index c39b614e7bad..89a58de096a1 100644 --- a/doc/users/prev_whats_new/github_stats_3.5.0.rst +++ b/doc/release/prev_whats_new/github_stats_3.5.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.5.0 + .. _github-stats-3-5-0: GitHub statistics for 3.5.0 (Nov 15, 2021) diff --git a/doc/users/prev_whats_new/github_stats_3.5.1.rst b/doc/release/prev_whats_new/github_stats_3.5.1.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.5.1.rst rename to doc/release/prev_whats_new/github_stats_3.5.1.rst index 626cf319c23c..dba71812c76c 100644 --- a/doc/users/prev_whats_new/github_stats_3.5.1.rst +++ b/doc/release/prev_whats_new/github_stats_3.5.1.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.5.1 + .. _github-stats-3-5-1: GitHub statistics for 3.5.1 (Dec 11, 2021) diff --git a/doc/users/prev_whats_new/github_stats_3.5.2.rst b/doc/release/prev_whats_new/github_stats_3.5.2.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.5.2.rst rename to doc/release/prev_whats_new/github_stats_3.5.2.rst index 66f53d8e3672..15e5067de7a6 100644 --- a/doc/users/prev_whats_new/github_stats_3.5.2.rst +++ b/doc/release/prev_whats_new/github_stats_3.5.2.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.5.2 + .. _github-stats-3-5-2: GitHub statistics for 3.5.2 (May 02, 2022) diff --git a/doc/users/prev_whats_new/github_stats_3.5.3.rst b/doc/release/prev_whats_new/github_stats_3.5.3.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.5.3.rst rename to doc/release/prev_whats_new/github_stats_3.5.3.rst index bafd6d5c27eb..e8c22ccc63a2 100644 --- a/doc/users/prev_whats_new/github_stats_3.5.3.rst +++ b/doc/release/prev_whats_new/github_stats_3.5.3.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.5.3 + .. _github-stats-3-5-3: GitHub statistics for 3.5.3 (Aug 10, 2022) diff --git a/doc/users/prev_whats_new/github_stats_3.6.0.rst b/doc/release/prev_whats_new/github_stats_3.6.0.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.6.0.rst rename to doc/release/prev_whats_new/github_stats_3.6.0.rst index 6764c7817741..32f373c13c78 100644 --- a/doc/users/prev_whats_new/github_stats_3.6.0.rst +++ b/doc/release/prev_whats_new/github_stats_3.6.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.6.0 + .. _github-stats-3-6-0: GitHub statistics for 3.6.0 (Sep 15, 2022) diff --git a/doc/users/prev_whats_new/github_stats_3.6.1.rst b/doc/release/prev_whats_new/github_stats_3.6.1.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.6.1.rst rename to doc/release/prev_whats_new/github_stats_3.6.1.rst index d47dc28fa076..94167402172f 100644 --- a/doc/users/prev_whats_new/github_stats_3.6.1.rst +++ b/doc/release/prev_whats_new/github_stats_3.6.1.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.6.1 + .. _github-stats-3-6-1: GitHub statistics for 3.6.1 (Oct 08, 2022) diff --git a/doc/users/prev_whats_new/github_stats_3.6.2.rst b/doc/release/prev_whats_new/github_stats_3.6.2.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.6.2.rst rename to doc/release/prev_whats_new/github_stats_3.6.2.rst index f633448aeaf1..e3b75268c528 100644 --- a/doc/users/prev_whats_new/github_stats_3.6.2.rst +++ b/doc/release/prev_whats_new/github_stats_3.6.2.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.6.2 + .. _github-stats-3-6-2: GitHub statistics for 3.6.2 (Nov 02, 2022) diff --git a/doc/users/prev_whats_new/github_stats_3.6.3.rst b/doc/release/prev_whats_new/github_stats_3.6.3.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.6.3.rst rename to doc/release/prev_whats_new/github_stats_3.6.3.rst index b1d17a791c87..edd3f63c38c5 100644 --- a/doc/users/prev_whats_new/github_stats_3.6.3.rst +++ b/doc/release/prev_whats_new/github_stats_3.6.3.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.6.3 + .. _github-stats-3-6-3: GitHub statistics for 3.6.3 (Jan 11, 2023) diff --git a/doc/users/prev_whats_new/github_stats_3.7.0.rst b/doc/release/prev_whats_new/github_stats_3.7.0.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.7.0.rst rename to doc/release/prev_whats_new/github_stats_3.7.0.rst index 754c4c1fc059..88140a2bc021 100644 --- a/doc/users/prev_whats_new/github_stats_3.7.0.rst +++ b/doc/release/prev_whats_new/github_stats_3.7.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.7.0 + .. _github-stats-3-7-0: GitHub statistics for 3.7.0 (Feb 13, 2023) diff --git a/doc/users/prev_whats_new/github_stats_3.7.1.rst b/doc/release/prev_whats_new/github_stats_3.7.1.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.7.1.rst rename to doc/release/prev_whats_new/github_stats_3.7.1.rst index b187122cb779..9147ff4e6ac1 100644 --- a/doc/users/prev_whats_new/github_stats_3.7.1.rst +++ b/doc/release/prev_whats_new/github_stats_3.7.1.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.7.1 + .. _github-stats-3-7-1: GitHub statistics for 3.7.1 (Mar 03, 2023) diff --git a/doc/users/prev_whats_new/github_stats_3.7.2.rst b/doc/release/prev_whats_new/github_stats_3.7.2.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.7.2.rst rename to doc/release/prev_whats_new/github_stats_3.7.2.rst index 9bc8ab85fdfd..500ce807f524 100644 --- a/doc/users/prev_whats_new/github_stats_3.7.2.rst +++ b/doc/release/prev_whats_new/github_stats_3.7.2.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.7.2 + .. _github-stats-3-7-2: GitHub statistics for 3.7.2 (Jul 05, 2023) diff --git a/doc/users/prev_whats_new/github_stats_3.7.3.rst b/doc/release/prev_whats_new/github_stats_3.7.3.rst similarity index 98% rename from doc/users/prev_whats_new/github_stats_3.7.3.rst rename to doc/release/prev_whats_new/github_stats_3.7.3.rst index bb43c1a8395e..f5b0ab256996 100644 --- a/doc/users/prev_whats_new/github_stats_3.7.3.rst +++ b/doc/release/prev_whats_new/github_stats_3.7.3.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.7.3 + .. _github-stats-3-7-3: GitHub statistics for 3.7.3 (Sep 11, 2023) diff --git a/doc/users/prev_whats_new/github_stats_3.8.0.rst b/doc/release/prev_whats_new/github_stats_3.8.0.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.8.0.rst rename to doc/release/prev_whats_new/github_stats_3.8.0.rst index 219e60f399ac..589093455af5 100644 --- a/doc/users/prev_whats_new/github_stats_3.8.0.rst +++ b/doc/release/prev_whats_new/github_stats_3.8.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.8.0 + .. _github-stats-3-8-0: GitHub statistics for 3.8.0 (Sep 14, 2023) diff --git a/doc/users/prev_whats_new/github_stats_3.8.1.rst b/doc/release/prev_whats_new/github_stats_3.8.1.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.8.1.rst rename to doc/release/prev_whats_new/github_stats_3.8.1.rst index 86de0e3b70a9..5043f5b641f1 100644 --- a/doc/users/prev_whats_new/github_stats_3.8.1.rst +++ b/doc/release/prev_whats_new/github_stats_3.8.1.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.8.1 + .. _github-stats-3-8-1: GitHub statistics for 3.8.1 (Oct 31, 2023) diff --git a/doc/users/prev_whats_new/github_stats_3.8.2.rst b/doc/release/prev_whats_new/github_stats_3.8.2.rst similarity index 98% rename from doc/users/prev_whats_new/github_stats_3.8.2.rst rename to doc/release/prev_whats_new/github_stats_3.8.2.rst index 0e5852be394b..1703c2e1bbb4 100644 --- a/doc/users/prev_whats_new/github_stats_3.8.2.rst +++ b/doc/release/prev_whats_new/github_stats_3.8.2.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.8.2 + .. _github-stats-3-8-2: GitHub statistics for 3.8.2 (Nov 17, 2023) diff --git a/doc/users/prev_whats_new/github_stats_3.8.3.rst b/doc/release/prev_whats_new/github_stats_3.8.3.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.8.3.rst rename to doc/release/prev_whats_new/github_stats_3.8.3.rst index c91e046fd6ae..c43a215bb869 100644 --- a/doc/users/prev_whats_new/github_stats_3.8.3.rst +++ b/doc/release/prev_whats_new/github_stats_3.8.3.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.8.3 + .. _github-stats-3-8-3: GitHub statistics for 3.8.3 (Feb 14, 2024) diff --git a/doc/users/prev_whats_new/github_stats_3.8.4.rst b/doc/release/prev_whats_new/github_stats_3.8.4.rst similarity index 98% rename from doc/users/prev_whats_new/github_stats_3.8.4.rst rename to doc/release/prev_whats_new/github_stats_3.8.4.rst index 324393b12f9d..9b38d03e8464 100644 --- a/doc/users/prev_whats_new/github_stats_3.8.4.rst +++ b/doc/release/prev_whats_new/github_stats_3.8.4.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.8.4 + .. _github-stats-3-8-4: GitHub statistics for 3.8.4 (Apr 03, 2024) diff --git a/doc/users/prev_whats_new/github_stats_3.9.0.rst b/doc/release/prev_whats_new/github_stats_3.9.0.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.9.0.rst rename to doc/release/prev_whats_new/github_stats_3.9.0.rst index 5ddbdfd6f2bd..cd84acc8e288 100644 --- a/doc/users/prev_whats_new/github_stats_3.9.0.rst +++ b/doc/release/prev_whats_new/github_stats_3.9.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.9.0 + .. _github-stats-3-9-0: GitHub statistics for 3.9.0 (May 15, 2024) diff --git a/doc/users/prev_whats_new/github_stats_3.9.1.rst b/doc/release/prev_whats_new/github_stats_3.9.1.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.9.1.rst rename to doc/release/prev_whats_new/github_stats_3.9.1.rst index 1bd7860546cb..58848b388f0c 100644 --- a/doc/users/prev_whats_new/github_stats_3.9.1.rst +++ b/doc/release/prev_whats_new/github_stats_3.9.1.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.9.1 + .. _github-stats-3-9-1: GitHub statistics for 3.9.1 (Jul 04, 2024) diff --git a/doc/users/prev_whats_new/github_stats_3.9.2.rst b/doc/release/prev_whats_new/github_stats_3.9.2.rst similarity index 98% rename from doc/users/prev_whats_new/github_stats_3.9.2.rst rename to doc/release/prev_whats_new/github_stats_3.9.2.rst index 542e0d81ce32..eb11dd62c3f3 100644 --- a/doc/users/prev_whats_new/github_stats_3.9.2.rst +++ b/doc/release/prev_whats_new/github_stats_3.9.2.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.9.2 + .. _github-stats-3-9-2: GitHub statistics for 3.9.2 (Aug 12, 2024) diff --git a/doc/users/prev_whats_new/github_stats_3.9.3.rst b/doc/release/prev_whats_new/github_stats_3.9.3.rst similarity index 99% rename from doc/users/prev_whats_new/github_stats_3.9.3.rst rename to doc/release/prev_whats_new/github_stats_3.9.3.rst index 06f0232c338c..f0b8a7b59c0d 100644 --- a/doc/users/prev_whats_new/github_stats_3.9.3.rst +++ b/doc/release/prev_whats_new/github_stats_3.9.3.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.9.3 + .. _github-stats-3-9-3: GitHub statistics for 3.9.3 (Nov 30, 2024) diff --git a/doc/users/prev_whats_new/github_stats_3.9.4.rst b/doc/release/prev_whats_new/github_stats_3.9.4.rst similarity index 94% rename from doc/users/prev_whats_new/github_stats_3.9.4.rst rename to doc/release/prev_whats_new/github_stats_3.9.4.rst index a821d2fc1f57..afedb6eccc27 100644 --- a/doc/users/prev_whats_new/github_stats_3.9.4.rst +++ b/doc/release/prev_whats_new/github_stats_3.9.4.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/github_stats_3.9.4 + .. _github-stats-3-9-4: GitHub statistics for 3.9.4 (Dec 13, 2024) diff --git a/doc/users/prev_whats_new/whats_new_0.98.4.rst b/doc/release/prev_whats_new/whats_new_0.98.4.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_0.98.4.rst rename to doc/release/prev_whats_new/whats_new_0.98.4.rst index 88d376cf79bf..8091c1277a5d 100644 --- a/doc/users/prev_whats_new/whats_new_0.98.4.rst +++ b/doc/release/prev_whats_new/whats_new_0.98.4.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_0.98.4 + .. _whats-new-0-98-4: What's new in Matplotlib 0.98.4 @@ -136,7 +138,7 @@ psd amplitude scaling Ryan May did a lot of work to rationalize the amplitude scaling of :func:`~matplotlib.pyplot.psd` and friends. See -:doc:`/gallery/lines_bars_and_markers/psd_demo`. +:doc:`/gallery/statistics/psd_demo`. The changes should increase MATLAB compatibility and increase scaling options. diff --git a/doc/users/prev_whats_new/whats_new_0.99.rst b/doc/release/prev_whats_new/whats_new_0.99.rst similarity index 98% rename from doc/users/prev_whats_new/whats_new_0.99.rst rename to doc/release/prev_whats_new/whats_new_0.99.rst index 47e4b18ae62d..94996a24c50b 100644 --- a/doc/users/prev_whats_new/whats_new_0.99.rst +++ b/doc/release/prev_whats_new/whats_new_0.99.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_0.99 + .. _whats-new-0-99: What's new in Matplotlib 0.99 (Aug 29, 2009) diff --git a/doc/users/prev_whats_new/whats_new_1.0.rst b/doc/release/prev_whats_new/whats_new_1.0.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_1.0.rst rename to doc/release/prev_whats_new/whats_new_1.0.rst index 772f241f4b23..99d40b3923b6 100644 --- a/doc/users/prev_whats_new/whats_new_1.0.rst +++ b/doc/release/prev_whats_new/whats_new_1.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_1.0 + .. _whats-new-1-0: What's new in Matplotlib 1.0 (Jul 06, 2010) diff --git a/doc/users/prev_whats_new/whats_new_1.1.rst b/doc/release/prev_whats_new/whats_new_1.1.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_1.1.rst rename to doc/release/prev_whats_new/whats_new_1.1.rst index 3f48fc9f87b6..1e036fbae095 100644 --- a/doc/users/prev_whats_new/whats_new_1.1.rst +++ b/doc/release/prev_whats_new/whats_new_1.1.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_1.1 + .. _whats-new-1-1: What's new in Matplotlib 1.1 (Nov 02, 2011) diff --git a/doc/users/prev_whats_new/whats_new_1.2.2.rst b/doc/release/prev_whats_new/whats_new_1.2.2.rst similarity index 90% rename from doc/users/prev_whats_new/whats_new_1.2.2.rst rename to doc/release/prev_whats_new/whats_new_1.2.2.rst index ab81018925cd..15e076e6cfaa 100644 --- a/doc/users/prev_whats_new/whats_new_1.2.2.rst +++ b/doc/release/prev_whats_new/whats_new_1.2.2.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_1.2.2 + .. _whats-new-1-2-2: What's new in Matplotlib 1.2.2 diff --git a/doc/users/prev_whats_new/whats_new_1.2.rst b/doc/release/prev_whats_new/whats_new_1.2.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_1.2.rst rename to doc/release/prev_whats_new/whats_new_1.2.rst index 43c729999d5b..7e25f60d632b 100644 --- a/doc/users/prev_whats_new/whats_new_1.2.rst +++ b/doc/release/prev_whats_new/whats_new_1.2.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_1.2 + .. _whats-new-1-2: diff --git a/doc/users/prev_whats_new/whats_new_1.3.rst b/doc/release/prev_whats_new/whats_new_1.3.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_1.3.rst rename to doc/release/prev_whats_new/whats_new_1.3.rst index af40f37f92b7..f5c7165538aa 100644 --- a/doc/users/prev_whats_new/whats_new_1.3.rst +++ b/doc/release/prev_whats_new/whats_new_1.3.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_1.3 + .. _whats-new-1-3: What's new in Matplotlib 1.3 (Aug 01, 2013) diff --git a/doc/users/prev_whats_new/whats_new_1.4.rst b/doc/release/prev_whats_new/whats_new_1.4.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_1.4.rst rename to doc/release/prev_whats_new/whats_new_1.4.rst index eb0e93fd8883..994e12ec977b 100644 --- a/doc/users/prev_whats_new/whats_new_1.4.rst +++ b/doc/release/prev_whats_new/whats_new_1.4.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_1.4 + .. _whats-new-1-4: diff --git a/doc/users/prev_whats_new/whats_new_1.5.rst b/doc/release/prev_whats_new/whats_new_1.5.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_1.5.rst rename to doc/release/prev_whats_new/whats_new_1.5.rst index 5bb4d4b9b5e9..8de98aedb01d 100644 --- a/doc/users/prev_whats_new/whats_new_1.5.rst +++ b/doc/release/prev_whats_new/whats_new_1.5.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_1.5 + .. _whats-new-1-5: What's new in Matplotlib 1.5 (Oct 29, 2015) diff --git a/doc/users/prev_whats_new/whats_new_2.0.0.rst b/doc/release/prev_whats_new/whats_new_2.0.0.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_2.0.0.rst rename to doc/release/prev_whats_new/whats_new_2.0.0.rst index 0f5edb7c0e3f..e6eea1984707 100644 --- a/doc/users/prev_whats_new/whats_new_2.0.0.rst +++ b/doc/release/prev_whats_new/whats_new_2.0.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_2.0.0 + .. _whats-new-2-0-0: What's new in Matplotlib 2.0 (Jan 17, 2017) diff --git a/doc/users/prev_whats_new/whats_new_2.1.0.rst b/doc/release/prev_whats_new/whats_new_2.1.0.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_2.1.0.rst rename to doc/release/prev_whats_new/whats_new_2.1.0.rst index a66e2e10f3a2..426ce377b7d1 100644 --- a/doc/users/prev_whats_new/whats_new_2.1.0.rst +++ b/doc/release/prev_whats_new/whats_new_2.1.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_2.1.0 + .. _whats-new-2-1-0: What's new in Matplotlib 2.1.0 (Oct 7, 2017) diff --git a/doc/users/prev_whats_new/whats_new_2.2.rst b/doc/release/prev_whats_new/whats_new_2.2.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_2.2.rst rename to doc/release/prev_whats_new/whats_new_2.2.rst index 6354a390860a..066b64d19cca 100644 --- a/doc/users/prev_whats_new/whats_new_2.2.rst +++ b/doc/release/prev_whats_new/whats_new_2.2.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_2.2 + .. _whats-new-2-2-0: What's new in Matplotlib 2.2 (Mar 06, 2018) diff --git a/doc/users/prev_whats_new/whats_new_3.0.rst b/doc/release/prev_whats_new/whats_new_3.0.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_3.0.rst rename to doc/release/prev_whats_new/whats_new_3.0.rst index e3dd12c71a8e..207c9a5eacd5 100644 --- a/doc/users/prev_whats_new/whats_new_3.0.rst +++ b/doc/release/prev_whats_new/whats_new_3.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_3.0 + .. _whats-new-3-0-0: What's new in Matplotlib 3.0 (Sep 18, 2018) diff --git a/doc/users/prev_whats_new/whats_new_3.1.0.rst b/doc/release/prev_whats_new/whats_new_3.1.0.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_3.1.0.rst rename to doc/release/prev_whats_new/whats_new_3.1.0.rst index 9f53435b89f6..689b035209bc 100644 --- a/doc/users/prev_whats_new/whats_new_3.1.0.rst +++ b/doc/release/prev_whats_new/whats_new_3.1.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_3.1.0 + .. _whats-new-3-1-0: What's new in Matplotlib 3.1 (May 18, 2019) diff --git a/doc/users/prev_whats_new/whats_new_3.10.0.rst b/doc/release/prev_whats_new/whats_new_3.10.0.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_3.10.0.rst rename to doc/release/prev_whats_new/whats_new_3.10.0.rst index f1231be53cc4..232ab6036100 100644 --- a/doc/users/prev_whats_new/whats_new_3.10.0.rst +++ b/doc/release/prev_whats_new/whats_new_3.10.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_3.10.0 + =================================================== What's new in Matplotlib 3.10.0 (December 13, 2024) =================================================== @@ -60,13 +62,6 @@ colour maps version 8.0.1 (DOI: https://doi.org/10.5281/zenodo.1243862). ax[2].imshow(img, cmap="vanimo") - -Plotting and Annotation improvements -==================================== - - - - Plotting and Annotation improvements ==================================== diff --git a/doc/users/prev_whats_new/whats_new_3.2.0.rst b/doc/release/prev_whats_new/whats_new_3.2.0.rst similarity index 98% rename from doc/users/prev_whats_new/whats_new_3.2.0.rst rename to doc/release/prev_whats_new/whats_new_3.2.0.rst index 12d7fab3af90..4fcba4e5a0fc 100644 --- a/doc/users/prev_whats_new/whats_new_3.2.0.rst +++ b/doc/release/prev_whats_new/whats_new_3.2.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_3.2.0 + .. _whats-new-3-2-0: What's new in Matplotlib 3.2 (Mar 04, 2020) diff --git a/doc/users/prev_whats_new/whats_new_3.3.0.rst b/doc/release/prev_whats_new/whats_new_3.3.0.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_3.3.0.rst rename to doc/release/prev_whats_new/whats_new_3.3.0.rst index 94914bcc75db..86ee971792e4 100644 --- a/doc/users/prev_whats_new/whats_new_3.3.0.rst +++ b/doc/release/prev_whats_new/whats_new_3.3.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_3.3.0 + .. _whats-new-3-3-0: ============================================= diff --git a/doc/users/prev_whats_new/whats_new_3.4.0.rst b/doc/release/prev_whats_new/whats_new_3.4.0.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_3.4.0.rst rename to doc/release/prev_whats_new/whats_new_3.4.0.rst index 003cd85fa49d..3cddee85b56e 100644 --- a/doc/users/prev_whats_new/whats_new_3.4.0.rst +++ b/doc/release/prev_whats_new/whats_new_3.4.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_3.4.0 + .. _whats-new-3-4-0: ============================================= diff --git a/doc/users/prev_whats_new/whats_new_3.5.0.rst b/doc/release/prev_whats_new/whats_new_3.5.0.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_3.5.0.rst rename to doc/release/prev_whats_new/whats_new_3.5.0.rst index fb156d0c68e8..d43a390d2db9 100644 --- a/doc/users/prev_whats_new/whats_new_3.5.0.rst +++ b/doc/release/prev_whats_new/whats_new_3.5.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_3.5.0 + ============================================= What's new in Matplotlib 3.5.0 (Nov 15, 2021) ============================================= diff --git a/doc/users/prev_whats_new/whats_new_3.5.2.rst b/doc/release/prev_whats_new/whats_new_3.5.2.rst similarity index 90% rename from doc/users/prev_whats_new/whats_new_3.5.2.rst rename to doc/release/prev_whats_new/whats_new_3.5.2.rst index 85b1c38862eb..98880653c9d8 100644 --- a/doc/users/prev_whats_new/whats_new_3.5.2.rst +++ b/doc/release/prev_whats_new/whats_new_3.5.2.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_3.5.2 + ============================================= What's new in Matplotlib 3.5.2 (May 02, 2022) ============================================= diff --git a/doc/users/prev_whats_new/whats_new_3.6.0.rst b/doc/release/prev_whats_new/whats_new_3.6.0.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_3.6.0.rst rename to doc/release/prev_whats_new/whats_new_3.6.0.rst index 9fcf8cebfc6f..57b162ca159d 100644 --- a/doc/users/prev_whats_new/whats_new_3.6.0.rst +++ b/doc/release/prev_whats_new/whats_new_3.6.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_3.6.0 + ============================================= What's new in Matplotlib 3.6.0 (Sep 15, 2022) ============================================= diff --git a/doc/users/prev_whats_new/whats_new_3.7.0.rst b/doc/release/prev_whats_new/whats_new_3.7.0.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_3.7.0.rst rename to doc/release/prev_whats_new/whats_new_3.7.0.rst index 1834cbf3726f..d2451bfa50bc 100644 --- a/doc/users/prev_whats_new/whats_new_3.7.0.rst +++ b/doc/release/prev_whats_new/whats_new_3.7.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_3.7.0 + ============================================= What's new in Matplotlib 3.7.0 (Feb 13, 2023) ============================================= diff --git a/doc/users/prev_whats_new/whats_new_3.8.0.rst b/doc/release/prev_whats_new/whats_new_3.8.0.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_3.8.0.rst rename to doc/release/prev_whats_new/whats_new_3.8.0.rst index fe1d5f7a7952..2d5ffe3ad3e7 100644 --- a/doc/users/prev_whats_new/whats_new_3.8.0.rst +++ b/doc/release/prev_whats_new/whats_new_3.8.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_3.8.0 + ============================================== What's new in Matplotlib 3.8.0 (Sept 13, 2023) ============================================== diff --git a/doc/users/prev_whats_new/whats_new_3.9.0.rst b/doc/release/prev_whats_new/whats_new_3.9.0.rst similarity index 99% rename from doc/users/prev_whats_new/whats_new_3.9.0.rst rename to doc/release/prev_whats_new/whats_new_3.9.0.rst index 85fabf86efbe..259bd2f35ee4 100644 --- a/doc/users/prev_whats_new/whats_new_3.9.0.rst +++ b/doc/release/prev_whats_new/whats_new_3.9.0.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/prev_whats_new/whats_new_3.9.0 + ============================================= What's new in Matplotlib 3.9.0 (May 15, 2024) ============================================= diff --git a/doc/users/release_notes.rst b/doc/release/release_notes.rst similarity index 96% rename from doc/users/release_notes.rst rename to doc/release/release_notes.rst index ae06d9875988..8ea4ad3d4f57 100644 --- a/doc/users/release_notes.rst +++ b/doc/release/release_notes.rst @@ -1,5 +1,6 @@ .. redirect-from:: /api/api_changes_old .. redirect-from:: /users/whats_new_old +.. redirect-from:: /users/release_notes .. _release-notes: @@ -18,9 +19,15 @@ Version 3.10 :maxdepth: 1 prev_whats_new/whats_new_3.10.0.rst + ../api/prev_api_changes/api_changes_3.10.7.rst ../api/prev_api_changes/api_changes_3.10.1.rst ../api/prev_api_changes/api_changes_3.10.0.rst github_stats.rst + prev_whats_new/github_stats_3.10.7.rst + prev_whats_new/github_stats_3.10.6.rst + prev_whats_new/github_stats_3.10.5.rst + prev_whats_new/github_stats_3.10.3.rst + prev_whats_new/github_stats_3.10.1.rst prev_whats_new/github_stats_3.10.0.rst Version 3.9 diff --git a/doc/users/release_notes_next.rst b/doc/release/release_notes_next.rst similarity index 73% rename from doc/users/release_notes_next.rst rename to doc/release/release_notes_next.rst index 6813f61c5f90..de10d5e8dc27 100644 --- a/doc/users/release_notes_next.rst +++ b/doc/release/release_notes_next.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/release_notes_next + :orphan: Next version diff --git a/doc/sphinxext/redirect_from.py b/doc/sphinxext/redirect_from.py index 37b56373a3bf..329352b3a3c8 100644 --- a/doc/sphinxext/redirect_from.py +++ b/doc/sphinxext/redirect_from.py @@ -94,10 +94,15 @@ def run(self): domain = self.env.get_domain('redirect_from') current_doc = self.env.path2doc(self.state.document.current_source) redirected_reldoc, _ = self.env.relfn2path(redirected_doc, current_doc) - if redirected_reldoc in domain.redirects: + if ( + redirected_reldoc in domain.redirects + and domain.redirects[redirected_reldoc] != current_doc + ): raise ValueError( f"{redirected_reldoc} is already noted as redirecting to " - f"{domain.redirects[redirected_reldoc]}") + f"{domain.redirects[redirected_reldoc]}\n" + f"Cannot also redirect it to {current_doc}" + ) domain.redirects[redirected_reldoc] = current_doc return [] diff --git a/doc/sphinxext/util.py b/doc/sphinxext/util.py index 14097ba9396a..c0f336eaea18 100644 --- a/doc/sphinxext/util.py +++ b/doc/sphinxext/util.py @@ -1,5 +1,7 @@ import sys +from sphinx_gallery import gen_rst + def matplotlib_reduced_latex_scraper(block, block_vars, gallery_conf, **kwargs): @@ -19,3 +21,33 @@ def matplotlib_reduced_latex_scraper(block, block_vars, gallery_conf, # Clear basic_units module to re-register with unit registry on import. def clear_basic_units(gallery_conf, fname): return sys.modules.pop('basic_units', None) + + +# Monkey-patching gallery header to include search keywords +EXAMPLE_HEADER = """ +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "{0}" +.. LINE NUMBERS ARE GIVEN BELOW. + +.. only:: html + + .. meta:: + :keywords: codex + + .. note:: + :class: sphx-glr-download-link-note + + :ref:`Go to the end ` + to download the full example code{2} + +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_{1}: + +""" + + +def patch_header(gallery_conf, fname): + gen_rst.EXAMPLE_HEADER = EXAMPLE_HEADER diff --git a/doc/users/_rcparams_generated.rst b/doc/users/_rcparams_generated.rst new file mode 100644 index 000000000000..19f972771ea9 --- /dev/null +++ b/doc/users/_rcparams_generated.rst @@ -0,0 +1,1674 @@ +.. + autogenerated rcParams documentation. Update via + > python -c "from matplotlib import rcsetup; rcsetup._write_rcParam_rst()" + + +.. _rcparam_webagg_port: + +webagg.port: ``8988`` + The port to use for the web server in the WebAgg backend. + +.. _rcparam_webagg_address: + +webagg.address: ``'127.0.0.1'`` + The address on which the WebAgg web server should be reachable. + +.. _rcparam_webagg_port_retries: + +webagg.port_retries: ``50`` + If webagg.port is unavailable, a number of other random ports will be tried until one that is available is found. + +.. _rcparam_webagg_open_in_browser: + +webagg.open_in_browser: ``True`` + When True, open the web browser to the plot that is shown + +.. _rcparam_backend_fallback: + +backend_fallback: ``True`` + If you are running pyplot inside a GUI and your backend choice conflicts, we will automatically try to find a compatible one for you if backend_fallback is True + +.. _rcparam_interactive: + +interactive: ``False`` + *no description* + +.. _rcparam_figure_hooks: + +figure.hooks: ``[]`` + list of dotted.module.name:dotted.callable.name + +.. _rcparam_toolbar: + +toolbar: ``'toolbar2'`` + {None, toolbar2, toolmanager} + +.. _rcparam_timezone: + +timezone: ``'UTC'`` + a pytz timezone string, e.g., US/Central or Europe/Paris + +.. _rcparam_lines_linewidth: + +lines.linewidth: ``1.5`` + line width in points + +.. _rcparam_lines_linestyle: + +lines.linestyle: ``'-'`` + solid line + +.. _rcparam_lines_color: + +lines.color: ``'C0'`` + has no affect on plot(); see axes.prop_cycle + +.. _rcparam_lines_marker: + +lines.marker: ``'None'`` + the default marker + +.. _rcparam_lines_markerfacecolor: + +lines.markerfacecolor: ``'auto'`` + the default marker face color + +.. _rcparam_lines_markeredgecolor: + +lines.markeredgecolor: ``'auto'`` + the default marker edge color + +.. _rcparam_lines_markeredgewidth: + +lines.markeredgewidth: ``1.0`` + the line width around the marker symbol + +.. _rcparam_lines_markersize: + +lines.markersize: ``6.0`` + marker size, in points + +.. _rcparam_lines_dash_joinstyle: + +lines.dash_joinstyle: ``'round'`` + {miter, round, bevel} + +.. _rcparam_lines_dash_capstyle: + +lines.dash_capstyle: ``'butt'`` + {butt, round, projecting} + +.. _rcparam_lines_solid_joinstyle: + +lines.solid_joinstyle: ``'round'`` + {miter, round, bevel} + +.. _rcparam_lines_solid_capstyle: + +lines.solid_capstyle: ``'projecting'`` + {butt, round, projecting} + +.. _rcparam_lines_antialiased: + +lines.antialiased: ``True`` + render lines in antialiased (no jaggies) + +.. _rcparam_lines_dashed_pattern: + +lines.dashed_pattern: ``[3.7, 1.6]`` + The dash pattern for linestyle 'dashed' + +.. _rcparam_lines_dashdot_pattern: + +lines.dashdot_pattern: ``[6.4, 1.6, 1.0, 1.6]`` + The dash pattern for linestyle 'dashdot' + +.. _rcparam_lines_dotted_pattern: + +lines.dotted_pattern: ``[1.0, 1.65]`` + The dash pattern for linestyle 'dotted' + +.. _rcparam_lines_scale_dashes: + +lines.scale_dashes: ``True`` + *no description* + +.. _rcparam_markers_fillstyle: + +markers.fillstyle: ``'full'`` + {full, left, right, bottom, top, none} + +.. _rcparam_pcolor_shading: + +pcolor.shading: ``'auto'`` + *no description* + +.. _rcparam_pcolormesh_snap: + +pcolormesh.snap: ``True`` + Whether to snap the mesh to pixel boundaries. This is provided solely to allow old test images to remain unchanged. Set to False to obtain the previous behavior. + +.. _rcparam_patch_linewidth: + +patch.linewidth: ``1.0`` + edge width in points. + +.. _rcparam_patch_facecolor: + +patch.facecolor: ``'C0'`` + *no description* + +.. _rcparam_patch_edgecolor: + +patch.edgecolor: ``'black'`` + By default, Patches and Collections do not draw edges. This value is only used if facecolor is "none" (an Artist without facecolor and edgecolor would be invisible) or if patch.force_edgecolor is True. + +.. _rcparam_patch_force_edgecolor: + +patch.force_edgecolor: ``False`` + By default, Patches and Collections do not draw edges. Set this to True to draw edges with patch.edgedcolor as the default edgecolor. This is mainly relevant for styles. + +.. _rcparam_patch_antialiased: + +patch.antialiased: ``True`` + render patches in antialiased (no jaggies) + +.. _rcparam_hatch_color: + +hatch.color: ``'edge'`` + *no description* + +.. _rcparam_hatch_linewidth: + +hatch.linewidth: ``1.0`` + *no description* + +.. _rcparam_boxplot_notch: + +boxplot.notch: ``False`` + *no description* + +.. _rcparam_boxplot_vertical: + +boxplot.vertical: ``True`` + *no description* + +.. _rcparam_boxplot_whiskers: + +boxplot.whiskers: ``1.5`` + *no description* + +.. _rcparam_boxplot_bootstrap: + +boxplot.bootstrap: ``None`` + *no description* + +.. _rcparam_boxplot_patchartist: + +boxplot.patchartist: ``False`` + *no description* + +.. _rcparam_boxplot_showmeans: + +boxplot.showmeans: ``False`` + *no description* + +.. _rcparam_boxplot_showcaps: + +boxplot.showcaps: ``True`` + *no description* + +.. _rcparam_boxplot_showbox: + +boxplot.showbox: ``True`` + *no description* + +.. _rcparam_boxplot_showfliers: + +boxplot.showfliers: ``True`` + *no description* + +.. _rcparam_boxplot_meanline: + +boxplot.meanline: ``False`` + *no description* + +.. _rcparam_boxplot_flierprops_color: + +boxplot.flierprops.color: ``'black'`` + *no description* + +.. _rcparam_boxplot_flierprops_marker: + +boxplot.flierprops.marker: ``'o'`` + *no description* + +.. _rcparam_boxplot_flierprops_markerfacecolor: + +boxplot.flierprops.markerfacecolor: ``'none'`` + *no description* + +.. _rcparam_boxplot_flierprops_markeredgecolor: + +boxplot.flierprops.markeredgecolor: ``'black'`` + *no description* + +.. _rcparam_boxplot_flierprops_markeredgewidth: + +boxplot.flierprops.markeredgewidth: ``1.0`` + *no description* + +.. _rcparam_boxplot_flierprops_markersize: + +boxplot.flierprops.markersize: ``6.0`` + *no description* + +.. _rcparam_boxplot_flierprops_linestyle: + +boxplot.flierprops.linestyle: ``'none'`` + *no description* + +.. _rcparam_boxplot_flierprops_linewidth: + +boxplot.flierprops.linewidth: ``1.0`` + *no description* + +.. _rcparam_boxplot_boxprops_color: + +boxplot.boxprops.color: ``'black'`` + *no description* + +.. _rcparam_boxplot_boxprops_linewidth: + +boxplot.boxprops.linewidth: ``1.0`` + *no description* + +.. _rcparam_boxplot_boxprops_linestyle: + +boxplot.boxprops.linestyle: ``'-'`` + *no description* + +.. _rcparam_boxplot_whiskerprops_color: + +boxplot.whiskerprops.color: ``'black'`` + *no description* + +.. _rcparam_boxplot_whiskerprops_linewidth: + +boxplot.whiskerprops.linewidth: ``1.0`` + *no description* + +.. _rcparam_boxplot_whiskerprops_linestyle: + +boxplot.whiskerprops.linestyle: ``'-'`` + *no description* + +.. _rcparam_boxplot_capprops_color: + +boxplot.capprops.color: ``'black'`` + *no description* + +.. _rcparam_boxplot_capprops_linewidth: + +boxplot.capprops.linewidth: ``1.0`` + *no description* + +.. _rcparam_boxplot_capprops_linestyle: + +boxplot.capprops.linestyle: ``'-'`` + *no description* + +.. _rcparam_boxplot_medianprops_color: + +boxplot.medianprops.color: ``'C1'`` + *no description* + +.. _rcparam_boxplot_medianprops_linewidth: + +boxplot.medianprops.linewidth: ``1.0`` + *no description* + +.. _rcparam_boxplot_medianprops_linestyle: + +boxplot.medianprops.linestyle: ``'-'`` + *no description* + +.. _rcparam_boxplot_meanprops_color: + +boxplot.meanprops.color: ``'C2'`` + *no description* + +.. _rcparam_boxplot_meanprops_marker: + +boxplot.meanprops.marker: ``'^'`` + *no description* + +.. _rcparam_boxplot_meanprops_markerfacecolor: + +boxplot.meanprops.markerfacecolor: ``'C2'`` + *no description* + +.. _rcparam_boxplot_meanprops_markeredgecolor: + +boxplot.meanprops.markeredgecolor: ``'C2'`` + *no description* + +.. _rcparam_boxplot_meanprops_markersize: + +boxplot.meanprops.markersize: ``6.0`` + *no description* + +.. _rcparam_boxplot_meanprops_linestyle: + +boxplot.meanprops.linestyle: ``'--'`` + *no description* + +.. _rcparam_boxplot_meanprops_linewidth: + +boxplot.meanprops.linewidth: ``1.0`` + *no description* + +.. _rcparam_font_family: + +font.family: ``['sans-serif']`` + *no description* + +.. _rcparam_font_style: + +font.style: ``'normal'`` + *no description* + +.. _rcparam_font_variant: + +font.variant: ``'normal'`` + *no description* + +.. _rcparam_font_weight: + +font.weight: ``'normal'`` + *no description* + +.. _rcparam_font_stretch: + +font.stretch: ``'normal'`` + *no description* + +.. _rcparam_font_size: + +font.size: ``10.0`` + *no description* + +.. _rcparam_font_serif: + +font.serif: ``['DejaVu Serif', 'Bitstream Vera Serif', 'Computer Modern Roman', 'New Century Schoolbook', 'Century Schoolbook L', 'Utopia', 'ITC Bookman', 'Bookman', 'Nimbus Roman No9 L', 'Times New Roman', 'Times', 'Palatino', 'Charter', 'serif']`` + *no description* + +.. _rcparam_font_sans-serif: + +font.sans-serif: ``['DejaVu Sans', 'Bitstream Vera Sans', 'Computer Modern Sans Serif', 'Lucida Grande', 'Verdana', 'Geneva', 'Lucid', 'Arial', 'Helvetica', 'Avant Garde', 'sans-serif']`` + *no description* + +.. _rcparam_font_cursive: + +font.cursive: ``['Apple Chancery', 'Textile', 'Zapf Chancery', 'Sand', 'Script MT', 'Felipa', 'Comic Neue', 'Comic Sans MS', 'cursive']`` + *no description* + +.. _rcparam_font_fantasy: + +font.fantasy: ``['Chicago', 'Charcoal', 'Impact', 'Western', 'xkcd script', 'fantasy']`` + *no description* + +.. _rcparam_font_monospace: + +font.monospace: ``['DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Computer Modern Typewriter', 'Andale Mono', 'Nimbus Mono L', 'Courier New', 'Courier', 'Fixed', 'Terminal', 'monospace']`` + *no description* + +.. _rcparam_font_enable_last_resort: + +font.enable_last_resort: ``True`` + If True, then Unicode Consortium's Last Resort font will be appended to all font selections. This ensures that there will always be a glyph displayed. + +.. _rcparam_text_color: + +text.color: ``'black'`` + *no description* + +.. _rcparam_text_hinting: + +text.hinting: ``'force_autohint'`` + FreeType hinting flag ("foo" corresponds to FT_LOAD_FOO); may be one of the following (Proprietary Matplotlib-specific synonyms are given in parentheses, but their use is discouraged): - default: Use the font's native hinter if possible, else FreeType's auto-hinter. ("either" is a synonym).- no_autohint: Use the font's native hinter if possible, else don't hint. ("native" is a synonym.)- force_autohint: Use FreeType's auto-hinter. ("auto" is a synonym.)- no_hinting: Disable hinting. ("none" is a synonym.) + +.. _rcparam_text_hinting_factor: + +text.hinting_factor: ``8`` + Specifies the amount of softness for hinting in the horizontal direction. A value of 1 will hint to full pixels. A value of 2 will hint to half pixels etc. + +.. _rcparam_text_kerning_factor: + +text.kerning_factor: ``0`` + Specifies the scaling factor for kerning values. This is provided solely to allow old test images to remain unchanged. Set to 6 to obtain previous behavior. Values other than 0 or 6 have no defined meaning. + +.. _rcparam_text_antialiased: + +text.antialiased: ``True`` + If True (default), the text will be antialiased. This only affects raster outputs. + +.. _rcparam_text_parse_math: + +text.parse_math: ``True`` + Use mathtext if there is an even number of unescaped dollar signs. + +.. _rcparam_text_usetex: + +text.usetex: ``False`` + use latex for all text handling. The following fonts are supported through the usual rc parameter settings: new century schoolbook, bookman, times, palatino, zapf chancery, charter, serif, sans-serif, helvetica, avant garde, courier, monospace, computer modern roman, computer modern sans serif, computer modern typewriter + +.. _rcparam_text_latex_preamble: + +text.latex.preamble: ``''`` + IMPROPER USE OF THIS FEATURE WILL LEAD TO LATEX FAILURES AND IS THEREFORE UNSUPPORTED. PLEASE DO NOT ASK FOR HELP IF THIS FEATURE DOES NOT DO WHAT YOU EXPECT IT TO. text.latex.preamble is a single line of LaTeX code that will be passed on to the LaTeX system. It may contain any code that is valid for the LaTeX "preamble", i.e. between the "\documentclass" and "\begin{document}" statements. Note that it has to be put on a single line, which may become quite long. The following packages are always loaded with usetex, so beware of package collisions: color, fix-cm, geometry, graphicx, textcomp. PostScript (PSNFSS) font packages may also be loaded, depending on your font settings. + +.. _rcparam_mathtext_fontset: + +mathtext.fontset: ``'dejavusans'`` + Should be 'dejavusans' (default), 'dejavuserif', 'cm' (Computer Modern), 'stix', 'stixsans' or 'custom' + +.. _rcparam_mathtext_bf: + +mathtext.bf: ``'sans:bold'`` + *no description* + +.. _rcparam_mathtext_bfit: + +mathtext.bfit: ``'sans:italic:bold'`` + *no description* + +.. _rcparam_mathtext_cal: + +mathtext.cal: ``'cursive'`` + *no description* + +.. _rcparam_mathtext_it: + +mathtext.it: ``'sans:italic'`` + *no description* + +.. _rcparam_mathtext_rm: + +mathtext.rm: ``'sans'`` + *no description* + +.. _rcparam_mathtext_sf: + +mathtext.sf: ``'sans'`` + *no description* + +.. _rcparam_mathtext_tt: + +mathtext.tt: ``'monospace'`` + *no description* + +.. _rcparam_mathtext_fallback: + +mathtext.fallback: ``'cm'`` + Select fallback font from ['cm' (Computer Modern), 'stix', 'stixsans'] when a symbol cannot be found in one of the custom math fonts. Select 'None' to not perform fallback and replace the missing character by a dummy symbol. + +.. _rcparam_mathtext_default: + +mathtext.default: ``'it'`` + The default font to use for math. Can be any of the LaTeX font names, including the special name "regular" for the same font used in regular text. + +.. _rcparam_axes_facecolor: + +axes.facecolor: ``'white'`` + axes background color + +.. _rcparam_axes_edgecolor: + +axes.edgecolor: ``'black'`` + axes edge color + +.. _rcparam_axes_linewidth: + +axes.linewidth: ``0.8`` + edge line width + +.. _rcparam_axes_grid: + +axes.grid: ``False`` + display grid or not + +.. _rcparam_axes_grid_axis: + +axes.grid.axis: ``'both'`` + which axis the grid should apply to + +.. _rcparam_axes_grid_which: + +axes.grid.which: ``'major'`` + grid lines at {major, minor, both} ticks + +.. _rcparam_axes_titlelocation: + +axes.titlelocation: ``'center'`` + alignment of the title: {left, right, center} + +.. _rcparam_axes_titlesize: + +axes.titlesize: ``'large'`` + font size of the axes title + +.. _rcparam_axes_titleweight: + +axes.titleweight: ``'normal'`` + font weight of title + +.. _rcparam_axes_titlecolor: + +axes.titlecolor: ``'auto'`` + color of the axes title, auto falls back to text.color as default value + +.. _rcparam_axes_titley: + +axes.titley: ``None`` + position title (axes relative units). None implies auto + +.. _rcparam_axes_titlepad: + +axes.titlepad: ``6.0`` + pad between axes and title in points + +.. _rcparam_axes_labelsize: + +axes.labelsize: ``'medium'`` + font size of the x and y labels + +.. _rcparam_axes_labelpad: + +axes.labelpad: ``4.0`` + space between label and axis + +.. _rcparam_axes_labelweight: + +axes.labelweight: ``'normal'`` + weight of the x and y labels + +.. _rcparam_axes_labelcolor: + +axes.labelcolor: ``'black'`` + *no description* + +.. _rcparam_axes_axisbelow: + +axes.axisbelow: ``'line'`` + draw axis gridlines and ticks: - below patches (True) - above patches but below lines ('line') - above all (False) + +.. _rcparam_axes_formatter_limits: + +axes.formatter.limits: ``[-5, 6]`` + use scientific notation if log10 of the axis range is smaller than the first or larger than the second + +.. _rcparam_axes_formatter_use_locale: + +axes.formatter.use_locale: ``False`` + When True, format tick labels according to the user's locale. For example, use ',' as a decimal separator in the fr_FR locale. + +.. _rcparam_axes_formatter_use_mathtext: + +axes.formatter.use_mathtext: ``False`` + When True, use mathtext for scientific notation. + +.. _rcparam_axes_formatter_min_exponent: + +axes.formatter.min_exponent: ``0`` + minimum exponent to format in scientific notation + +.. _rcparam_axes_formatter_useoffset: + +axes.formatter.useoffset: ``True`` + If True, the tick label formatter will default to labeling ticks relative to an offset when the data range is small compared to the minimum absolute value of the data. + +.. _rcparam_axes_formatter_offset_threshold: + +axes.formatter.offset_threshold: ``4`` + When useoffset is True, the offset will be used when it can remove at least this number of significant digits from tick labels. + +.. _rcparam_axes_spines_left: + +axes.spines.left: ``True`` + display axis spines + +.. _rcparam_axes_spines_bottom: + +axes.spines.bottom: ``True`` + *no description* + +.. _rcparam_axes_spines_top: + +axes.spines.top: ``True`` + *no description* + +.. _rcparam_axes_spines_right: + +axes.spines.right: ``True`` + *no description* + +.. _rcparam_axes_unicode_minus: + +axes.unicode_minus: ``True`` + use Unicode for the minus symbol rather than hyphen. See https://en.wikipedia.org/wiki/Plus_and_minus_signs#Character_codes + +.. _rcparam_axes_prop_cycle: + +axes.prop_cycle: ``cycler('color', [(0.12156862745098039, 0.4666666666666667, 0.7058823529411765), (1.0, 0.4980392156862745, 0.054901960784313725), (0.17254901960784313, 0.6274509803921569, 0.17254901960784313), (0.8392156862745098, 0.15294117647058825, 0.1568627450980392), (0.5803921568627451, 0.403921568627451, 0.7411764705882353), (0.5490196078431373, 0.33725490196078434, 0.29411764705882354), (0.8901960784313725, 0.4666666666666667, 0.7607843137254902), (0.4980392156862745, 0.4980392156862745, 0.4980392156862745), (0.7372549019607844, 0.7411764705882353, 0.13333333333333333), (0.09019607843137255, 0.7450980392156863, 0.8117647058823529)])`` + *no description* + +.. _rcparam_axes_xmargin: + +axes.xmargin: ``0.05`` + x margin. See `~.axes.Axes.margins` + +.. _rcparam_axes_ymargin: + +axes.ymargin: ``0.05`` + y margin. See `~.axes.Axes.margins` + +.. _rcparam_axes_zmargin: + +axes.zmargin: ``0.05`` + z margin. See `~.axes.Axes.margins` + +.. _rcparam_axes_autolimit_mode: + +axes.autolimit_mode: ``'data'`` + If "data", use axes.xmargin and axes.ymargin as is. If "round_numbers", after application of margins, axis limits are further expanded to the nearest "round" number. + +.. _rcparam_polaraxes_grid: + +polaraxes.grid: ``True`` + display grid on polar axes + +.. _rcparam_axes3d_grid: + +axes3d.grid: ``True`` + display grid on 3D axes + +.. _rcparam_axes3d_automargin: + +axes3d.automargin: ``False`` + automatically add margin when manually setting 3D axis limits + +.. _rcparam_axes3d_xaxis_panecolor: + +axes3d.xaxis.panecolor: ``(0.95, 0.95, 0.95, 0.5)`` + background pane on 3D axes + +.. _rcparam_axes3d_yaxis_panecolor: + +axes3d.yaxis.panecolor: ``(0.9, 0.9, 0.9, 0.5)`` + background pane on 3D axes + +.. _rcparam_axes3d_zaxis_panecolor: + +axes3d.zaxis.panecolor: ``(0.925, 0.925, 0.925, 0.5)`` + background pane on 3D axes + +.. _rcparam_axes3d_depthshade: + +axes3d.depthshade: ``True`` + depth shade for 3D scatter plots + +.. _rcparam_axes3d_depthshade_minalpha: + +axes3d.depthshade_minalpha: ``0.3`` + minimum alpha value for depth shading + +.. _rcparam_axes3d_mouserotationstyle: + +axes3d.mouserotationstyle: ``'arcball'`` + {azel, trackball, sphere, arcball} See also https://matplotlib.org/stable/api/toolkits/mplot3d/view_angles.html#rotation-with-mouse + +.. _rcparam_axes3d_trackballsize: + +axes3d.trackballsize: ``0.667`` + trackball diameter, in units of the Axes bbox + +.. _rcparam_axes3d_trackballborder: + +axes3d.trackballborder: ``0.2`` + trackball border width, in units of the Axes bbox (only for 'sphere' and 'arcball' style) + +.. _rcparam_xaxis_labellocation: + +xaxis.labellocation: ``'center'`` + alignment of the xaxis label: {left, right, center} + +.. _rcparam_yaxis_labellocation: + +yaxis.labellocation: ``'center'`` + alignment of the yaxis label: {bottom, top, center} + +.. _rcparam_date_autoformatter_year: + +date.autoformatter.year: ``'%Y'`` + *no description* + +.. _rcparam_date_autoformatter_month: + +date.autoformatter.month: ``'%Y-%m'`` + *no description* + +.. _rcparam_date_autoformatter_day: + +date.autoformatter.day: ``'%Y-%m-%d'`` + *no description* + +.. _rcparam_date_autoformatter_hour: + +date.autoformatter.hour: ``'%m-%d %H'`` + *no description* + +.. _rcparam_date_autoformatter_minute: + +date.autoformatter.minute: ``'%d %H:%M'`` + *no description* + +.. _rcparam_date_autoformatter_second: + +date.autoformatter.second: ``'%H:%M:%S'`` + *no description* + +.. _rcparam_date_autoformatter_microsecond: + +date.autoformatter.microsecond: ``'%M:%S.%f'`` + *no description* + +.. _rcparam_date_epoch: + +date.epoch: ``'1970-01-01T00:00:00'`` + The reference date for Matplotlib's internal date representation. See https://matplotlib.org/stable/gallery/ticks/date_precision_and_epochs.html + +.. _rcparam_date_converter: + +date.converter: ``'auto'`` + 'auto', 'concise' + +.. _rcparam_date_interval_multiples: + +date.interval_multiples: ``True`` + For auto converter whether to use interval_multiples + +.. _rcparam_xtick_top: + +xtick.top: ``False`` + draw ticks on the top side + +.. _rcparam_xtick_bottom: + +xtick.bottom: ``True`` + draw ticks on the bottom side + +.. _rcparam_xtick_labeltop: + +xtick.labeltop: ``False`` + draw label on the top + +.. _rcparam_xtick_labelbottom: + +xtick.labelbottom: ``True`` + draw label on the bottom + +.. _rcparam_xtick_major_size: + +xtick.major.size: ``3.5`` + major tick size in points + +.. _rcparam_xtick_minor_size: + +xtick.minor.size: ``2.0`` + minor tick size in points + +.. _rcparam_xtick_major_width: + +xtick.major.width: ``0.8`` + major tick width in points + +.. _rcparam_xtick_minor_width: + +xtick.minor.width: ``0.6`` + minor tick width in points + +.. _rcparam_xtick_major_pad: + +xtick.major.pad: ``3.5`` + distance to major tick label in points + +.. _rcparam_xtick_minor_pad: + +xtick.minor.pad: ``3.4`` + distance to the minor tick label in points + +.. _rcparam_xtick_color: + +xtick.color: ``'black'`` + color of the ticks + +.. _rcparam_xtick_labelcolor: + +xtick.labelcolor: ``'inherit'`` + color of the tick labels or inherit from xtick.color + +.. _rcparam_xtick_labelsize: + +xtick.labelsize: ``'medium'`` + font size of the tick labels + +.. _rcparam_xtick_direction: + +xtick.direction: ``'out'`` + direction: {in, out, inout} + +.. _rcparam_xtick_minor_visible: + +xtick.minor.visible: ``False`` + visibility of minor ticks on x-axis + +.. _rcparam_xtick_major_top: + +xtick.major.top: ``True`` + draw x axis top major ticks + +.. _rcparam_xtick_major_bottom: + +xtick.major.bottom: ``True`` + draw x axis bottom major ticks + +.. _rcparam_xtick_minor_top: + +xtick.minor.top: ``True`` + draw x axis top minor ticks + +.. _rcparam_xtick_minor_bottom: + +xtick.minor.bottom: ``True`` + draw x axis bottom minor ticks + +.. _rcparam_xtick_minor_ndivs: + +xtick.minor.ndivs: ``'auto'`` + number of minor ticks between the major ticks on x-axis + +.. _rcparam_xtick_alignment: + +xtick.alignment: ``'center'`` + alignment of xticks + +.. _rcparam_ytick_left: + +ytick.left: ``True`` + draw ticks on the left side + +.. _rcparam_ytick_right: + +ytick.right: ``False`` + draw ticks on the right side + +.. _rcparam_ytick_labelleft: + +ytick.labelleft: ``True`` + draw tick labels on the left side + +.. _rcparam_ytick_labelright: + +ytick.labelright: ``False`` + draw tick labels on the right side + +.. _rcparam_ytick_major_size: + +ytick.major.size: ``3.5`` + major tick size in points + +.. _rcparam_ytick_minor_size: + +ytick.minor.size: ``2.0`` + minor tick size in points + +.. _rcparam_ytick_major_width: + +ytick.major.width: ``0.8`` + major tick width in points + +.. _rcparam_ytick_minor_width: + +ytick.minor.width: ``0.6`` + minor tick width in points + +.. _rcparam_ytick_major_pad: + +ytick.major.pad: ``3.5`` + distance to major tick label in points + +.. _rcparam_ytick_minor_pad: + +ytick.minor.pad: ``3.4`` + distance to the minor tick label in points + +.. _rcparam_ytick_color: + +ytick.color: ``'black'`` + color of the ticks + +.. _rcparam_ytick_labelcolor: + +ytick.labelcolor: ``'inherit'`` + color of the tick labels or inherit from ytick.color + +.. _rcparam_ytick_labelsize: + +ytick.labelsize: ``'medium'`` + font size of the tick labels + +.. _rcparam_ytick_direction: + +ytick.direction: ``'out'`` + direction: {in, out, inout} + +.. _rcparam_ytick_minor_visible: + +ytick.minor.visible: ``False`` + visibility of minor ticks on y-axis + +.. _rcparam_ytick_major_left: + +ytick.major.left: ``True`` + draw y axis left major ticks + +.. _rcparam_ytick_major_right: + +ytick.major.right: ``True`` + draw y axis right major ticks + +.. _rcparam_ytick_minor_left: + +ytick.minor.left: ``True`` + draw y axis left minor ticks + +.. _rcparam_ytick_minor_right: + +ytick.minor.right: ``True`` + draw y axis right minor ticks + +.. _rcparam_ytick_minor_ndivs: + +ytick.minor.ndivs: ``'auto'`` + number of minor ticks between the major ticks on y-axis + +.. _rcparam_ytick_alignment: + +ytick.alignment: ``'center_baseline'`` + alignment of yticks + +.. _rcparam_grid_color: + +grid.color: ``'#b0b0b0'`` + b0b0b0" # grid color + +.. _rcparam_grid_linestyle: + +grid.linestyle: ``'-'`` + solid + +.. _rcparam_grid_linewidth: + +grid.linewidth: ``0.8`` + in points + +.. _rcparam_grid_alpha: + +grid.alpha: ``1.0`` + transparency, between 0.0 and 1.0 + +.. _rcparam_grid_major_color: + +grid.major.color: ``None`` + If None defaults to grid.color + +.. _rcparam_grid_major_linestyle: + +grid.major.linestyle: ``None`` + If None defaults to grid.linestyle + +.. _rcparam_grid_major_linewidth: + +grid.major.linewidth: ``None`` + If None defaults to grid.linewidth + +.. _rcparam_grid_major_alpha: + +grid.major.alpha: ``None`` + If None defaults to grid.alpha + +.. _rcparam_grid_minor_color: + +grid.minor.color: ``None`` + If None defaults to grid.color + +.. _rcparam_grid_minor_linestyle: + +grid.minor.linestyle: ``None`` + If None defaults to grid.linestyle + +.. _rcparam_grid_minor_linewidth: + +grid.minor.linewidth: ``None`` + If None defaults to grid.linewidth + +.. _rcparam_grid_minor_alpha: + +grid.minor.alpha: ``None`` + If None defaults to grid.alpha + +.. _rcparam_legend_loc: + +legend.loc: ``'best'`` + *no description* + +.. _rcparam_legend_frameon: + +legend.frameon: ``True`` + if True, draw the legend on a background patch + +.. _rcparam_legend_framealpha: + +legend.framealpha: ``0.8`` + legend patch transparency + +.. _rcparam_legend_facecolor: + +legend.facecolor: ``'inherit'`` + inherit from axes.facecolor; or color spec + +.. _rcparam_legend_edgecolor: + +legend.edgecolor: ``'0.8'`` + background patch boundary color + +.. _rcparam_legend_linewidth: + +legend.linewidth: ``None`` + line width of the legend frame, None means inherit from patch.linewidth + +.. _rcparam_legend_fancybox: + +legend.fancybox: ``True`` + if True, use a rounded box for the legend background, else a rectangle + +.. _rcparam_legend_shadow: + +legend.shadow: ``False`` + if True, give background a shadow effect + +.. _rcparam_legend_numpoints: + +legend.numpoints: ``1`` + the number of marker points in the legend line + +.. _rcparam_legend_scatterpoints: + +legend.scatterpoints: ``1`` + number of scatter points + +.. _rcparam_legend_markerscale: + +legend.markerscale: ``1.0`` + the relative size of legend markers vs. original + +.. _rcparam_legend_fontsize: + +legend.fontsize: ``'medium'`` + *no description* + +.. _rcparam_legend_labelcolor: + +legend.labelcolor: ``'None'`` + *no description* + +.. _rcparam_legend_title_fontsize: + +legend.title_fontsize: ``None`` + None sets to the same as the default axes. + +.. _rcparam_legend_borderpad: + +legend.borderpad: ``0.4`` + border whitespace + +.. _rcparam_legend_labelspacing: + +legend.labelspacing: ``0.5`` + the vertical space between the legend entries + +.. _rcparam_legend_handlelength: + +legend.handlelength: ``2.0`` + the length of the legend lines + +.. _rcparam_legend_handleheight: + +legend.handleheight: ``0.7`` + the height of the legend handle + +.. _rcparam_legend_handletextpad: + +legend.handletextpad: ``0.8`` + the space between the legend line and legend text + +.. _rcparam_legend_borderaxespad: + +legend.borderaxespad: ``0.5`` + the border between the axes and legend edge + +.. _rcparam_legend_columnspacing: + +legend.columnspacing: ``2.0`` + column separation + +.. _rcparam_figure_titlesize: + +figure.titlesize: ``'large'`` + size of the figure title (``Figure.suptitle()``) + +.. _rcparam_figure_titleweight: + +figure.titleweight: ``'normal'`` + weight of the figure title + +.. _rcparam_figure_labelsize: + +figure.labelsize: ``'large'`` + size of the figure label (``Figure.sup[x|y]label()``) + +.. _rcparam_figure_labelweight: + +figure.labelweight: ``'normal'`` + weight of the figure label + +.. _rcparam_figure_figsize: + +figure.figsize: ``[6.4, 4.8]`` + figure size in inches + +.. _rcparam_figure_dpi: + +figure.dpi: ``100.0`` + figure dots per inch + +.. _rcparam_figure_facecolor: + +figure.facecolor: ``'white'`` + figure face color + +.. _rcparam_figure_edgecolor: + +figure.edgecolor: ``'white'`` + figure edge color + +.. _rcparam_figure_frameon: + +figure.frameon: ``True`` + enable figure frame + +.. _rcparam_figure_max_open_warning: + +figure.max_open_warning: ``20`` + The maximum number of figures to open through the pyplot interface before emitting a warning. If less than one this feature is disabled. + +.. _rcparam_figure_raise_window: + +figure.raise_window: ``True`` + Raise the GUI window to front when show() is called. If set to False, we currently do not take any further actions and whether the window appears on the front may depend on the GUI framework and window manager. + +.. _rcparam_figure_subplot_left: + +figure.subplot.left: ``0.125`` + the left side of the subplots of the figure + +.. _rcparam_figure_subplot_right: + +figure.subplot.right: ``0.9`` + the right side of the subplots of the figure + +.. _rcparam_figure_subplot_bottom: + +figure.subplot.bottom: ``0.11`` + the bottom of the subplots of the figure + +.. _rcparam_figure_subplot_top: + +figure.subplot.top: ``0.88`` + the top of the subplots of the figure + +.. _rcparam_figure_subplot_wspace: + +figure.subplot.wspace: ``0.2`` + the amount of width reserved for space between subplots, expressed as a fraction of the average axis width + +.. _rcparam_figure_subplot_hspace: + +figure.subplot.hspace: ``0.2`` + the amount of height reserved for space between subplots, expressed as a fraction of the average axis height + +.. _rcparam_figure_autolayout: + +figure.autolayout: ``False`` + When True, automatically adjust subplot parameters to make the plot fit the figure using `~.Figure.tight_layout` + +.. _rcparam_figure_constrained_layout_use: + +figure.constrained_layout.use: ``False`` + When True, automatically make plot elements fit on the figure. (Not compatible with "figure.autolayout", above). + +.. _rcparam_figure_constrained_layout_h_pad: + +figure.constrained_layout.h_pad: ``0.04167`` + Padding (in inches) around axes; defaults to 3/72 inches, i.e. 3 points + +.. _rcparam_figure_constrained_layout_w_pad: + +figure.constrained_layout.w_pad: ``0.04167`` + Padding (in inches) around axes; defaults to 3/72 inches, i.e. 3 points + +.. _rcparam_figure_constrained_layout_hspace: + +figure.constrained_layout.hspace: ``0.02`` + Spacing between subplots, relative to the subplot sizes. Much smaller than for tight_layout (figure.subplot.hspace, figure.subplot.wspace) as constrained_layout already takes surrounding texts (titles, labels, # ticklabels) into account. + +.. _rcparam_figure_constrained_layout_wspace: + +figure.constrained_layout.wspace: ``0.02`` + Spacing between subplots, relative to the subplot sizes. Much smaller than for tight_layout (figure.subplot.hspace, figure.subplot.wspace) as constrained_layout already takes surrounding texts (titles, labels, # ticklabels) into account. + +.. _rcparam_image_aspect: + +image.aspect: ``'equal'`` + {equal, auto} or a number + +.. _rcparam_image_interpolation: + +image.interpolation: ``'auto'`` + see help(imshow) for options + +.. _rcparam_image_interpolation_stage: + +image.interpolation_stage: ``'auto'`` + see help(imshow) for options + +.. _rcparam_image_cmap: + +image.cmap: ``'viridis'`` + A colormap name (plasma, magma, etc.) + +.. _rcparam_image_lut: + +image.lut: ``256`` + the size of the colormap lookup table + +.. _rcparam_image_origin: + +image.origin: ``'upper'`` + {lower, upper} + +.. _rcparam_image_resample: + +image.resample: ``True`` + *no description* + +.. _rcparam_image_composite_image: + +image.composite_image: ``True`` + When True, all the images on a set of axes are combined into a single composite image before saving a figure as a vector graphics file, such as a PDF. + +.. _rcparam_contour_negative_linestyle: + +contour.negative_linestyle: ``'dashed'`` + string or on-off ink sequence + +.. _rcparam_contour_corner_mask: + +contour.corner_mask: ``True`` + {True, False} + +.. _rcparam_contour_linewidth: + +contour.linewidth: ``None`` + {float, None} Size of the contour line widths. If set to None, it falls back to "line.linewidth". + +.. _rcparam_contour_algorithm: + +contour.algorithm: ``'mpl2014'`` + {mpl2005, mpl2014, serial, threaded} + +.. _rcparam_errorbar_capsize: + +errorbar.capsize: ``0.0`` + length of end cap on error bars in pixels + +.. _rcparam_hist_bins: + +hist.bins: ``10`` + The default number of histogram bins or 'auto'. + +.. _rcparam_scatter_marker: + +scatter.marker: ``'o'`` + The default marker type for scatter plots. + +.. _rcparam_scatter_edgecolors: + +scatter.edgecolors: ``'face'`` + The default edge colors for scatter plots. + +.. _rcparam_agg_path_chunksize: + +agg.path.chunksize: ``0`` + 0 to disable; values in the range 10000 to 100000 can improve speed slightly and prevent an Agg rendering failure when plotting very large data sets, especially if they are very gappy. It may cause minor artifacts, though. A value of 20000 is probably a good starting point. + +.. _rcparam_path_simplify: + +path.simplify: ``True`` + When True, simplify paths by removing "invisible" points to reduce file size and increase rendering speed + +.. _rcparam_path_simplify_threshold: + +path.simplify_threshold: ``0.111111111111`` + The threshold of similarity below which vertices will be removed in the simplification process. + +.. _rcparam_path_snap: + +path.snap: ``True`` + When True, rectilinear axis-aligned paths will be snapped to the nearest pixel when certain criteria are met. When False, paths will never be snapped. + +.. _rcparam_path_sketch: + +path.sketch: ``None`` + May be None, or a tuple of the form:path.sketch: (scale, length, randomness)- *scale* is the amplitude of the wiggle perpendicular to the line (in pixels).- *length* is the length of the wiggle along the line (in pixels).- *randomness* is the factor by which the length is randomly scaled. + +.. _rcparam_path_effects: + +path.effects: ``[]`` + *no description* + +.. _rcparam_savefig_dpi: + +savefig.dpi: ``'figure'`` + figure dots per inch or 'figure' + +.. _rcparam_savefig_facecolor: + +savefig.facecolor: ``'auto'`` + figure face color when saving + +.. _rcparam_savefig_edgecolor: + +savefig.edgecolor: ``'auto'`` + figure edge color when saving + +.. _rcparam_savefig_format: + +savefig.format: ``'png'`` + {png, ps, pdf, svg} + +.. _rcparam_savefig_bbox: + +savefig.bbox: ``None`` + {tight, standard} 'tight' is incompatible with generating frames for animation + +.. _rcparam_savefig_pad_inches: + +savefig.pad_inches: ``0.1`` + padding to be used, when bbox is set to 'tight' + +.. _rcparam_savefig_directory: + +savefig.directory: ``'~'`` + default directory in savefig dialog, gets updated after interactive saves, unless set to the empty string (i.e. the current directory); use '.' to start at the current directory but update after interactive saves + +.. _rcparam_savefig_transparent: + +savefig.transparent: ``False`` + whether figures are saved with a transparent background by default + +.. _rcparam_savefig_orientation: + +savefig.orientation: ``'portrait'`` + orientation of saved figure, for PostScript output only + +.. _rcparam_macosx_window_mode: + +macosx.window_mode: ``'system'`` + How to open new figures (system, tab, window) system uses the MacOS system preferences + +.. _rcparam_tk_window_focus: + +tk.window_focus: ``False`` + Maintain shell focus for TkAgg + +.. _rcparam_ps_papersize: + +ps.papersize: ``'letter'`` + {figure, letter, legal, ledger, A0-A10, B0-B10} + +.. _rcparam_ps_useafm: + +ps.useafm: ``False`` + use AFM fonts, results in small files + +.. _rcparam_ps_usedistiller: + +ps.usedistiller: ``None`` + {ghostscript, xpdf, None} Experimental: may produce smaller files. xpdf intended for production of publication quality files, but requires ghostscript, xpdf and ps2eps + +.. _rcparam_ps_distiller_res: + +ps.distiller.res: ``6000`` + dpi + +.. _rcparam_ps_fonttype: + +ps.fonttype: ``3`` + Output Type 3 (Type3) or Type 42 (TrueType) + +.. _rcparam_pdf_compression: + +pdf.compression: ``6`` + integer from 0 to 9 0 disables compression (good for debugging) + +.. _rcparam_pdf_fonttype: + +pdf.fonttype: ``3`` + Output Type 3 (Type3) or Type 42 (TrueType) + +.. _rcparam_pdf_use14corefonts: + +pdf.use14corefonts: ``False`` + *no description* + +.. _rcparam_pdf_inheritcolor: + +pdf.inheritcolor: ``False`` + *no description* + +.. _rcparam_svg_image_inline: + +svg.image_inline: ``True`` + Write raster image data directly into the SVG file + +.. _rcparam_svg_fonttype: + +svg.fonttype: ``'path'`` + How to handle SVG fonts: path: Embed characters as paths -- supported by most SVG renderersnone: Assume fonts are installed on the machine where the SVG will be viewed. + +.. _rcparam_svg_hashsalt: + +svg.hashsalt: ``None`` + If not None, use this string as hash salt instead of uuid4 + +.. _rcparam_svg_id: + +svg.id: ``None`` + If not None, use this string as the value for the `id` attribute in the top tag + +.. _rcparam_pgf_rcfonts: + +pgf.rcfonts: ``True`` + *no description* + +.. _rcparam_pgf_preamble: + +pgf.preamble: ``''`` + See text.latex.preamble for documentation + +.. _rcparam_pgf_texsystem: + +pgf.texsystem: ``'xelatex'`` + *no description* + +.. _rcparam_docstring_hardcopy: + +docstring.hardcopy: ``False`` + set this when you want to generate hardcopy docstring + +.. _rcparam_keymap_fullscreen: + +keymap.fullscreen: ``['f', 'ctrl+f']`` + toggling + +.. _rcparam_keymap_home: + +keymap.home: ``['h', 'r', 'home']`` + home or reset mnemonic + +.. _rcparam_keymap_back: + +keymap.back: ``['left', 'c', 'backspace', 'MouseButton.BACK']`` + forward / backward keys + +.. _rcparam_keymap_forward: + +keymap.forward: ``['right', 'v', 'MouseButton.FORWARD']`` + for quick navigation + +.. _rcparam_keymap_pan: + +keymap.pan: ``['p']`` + pan mnemonic + +.. _rcparam_keymap_zoom: + +keymap.zoom: ``['o']`` + zoom mnemonic + +.. _rcparam_keymap_save: + +keymap.save: ``['s', 'ctrl+s']`` + saving current figure + +.. _rcparam_keymap_help: + +keymap.help: ``['f1']`` + display help about active tools + +.. _rcparam_keymap_quit: + +keymap.quit: ``['ctrl+w', 'cmd+w', 'q']`` + close the current figure + +.. _rcparam_keymap_quit_all: + +keymap.quit_all: ``[]`` + close all figures + +.. _rcparam_keymap_grid: + +keymap.grid: ``['g']`` + switching on/off major grids in current axes + +.. _rcparam_keymap_grid_minor: + +keymap.grid_minor: ``['G']`` + switching on/off minor grids in current axes + +.. _rcparam_keymap_yscale: + +keymap.yscale: ``['l']`` + toggle scaling of y-axes ('log'/'linear') + +.. _rcparam_keymap_xscale: + +keymap.xscale: ``['k', 'L']`` + toggle scaling of x-axes ('log'/'linear') + +.. _rcparam_keymap_copy: + +keymap.copy: ``['ctrl+c', 'cmd+c']`` + copy figure to clipboard + +.. _rcparam_animation_html: + +animation.html: ``'none'`` + How to display the animation as HTML in the IPython notebook: - 'html5' uses HTML5 video tag - 'jshtml' creates a JavaScript animation + +.. _rcparam_animation_writer: + +animation.writer: ``'ffmpeg'`` + MovieWriter 'backend' to use + +.. _rcparam_animation_codec: + +animation.codec: ``'h264'`` + Codec to use for writing movie + +.. _rcparam_animation_bitrate: + +animation.bitrate: ``-1`` + Controls size/quality trade-off for movie. -1 implies let utility auto-determine + +.. _rcparam_animation_frame_format: + +animation.frame_format: ``'png'`` + Controls frame format used by temp files + +.. _rcparam_animation_ffmpeg_path: + +animation.ffmpeg_path: ``'ffmpeg'`` + Path to ffmpeg binary. Unqualified paths are resolved by subprocess.Popen. + +.. _rcparam_animation_ffmpeg_args: + +animation.ffmpeg_args: ``[]`` + Additional arguments to pass to ffmpeg + +.. _rcparam_animation_convert_path: + +animation.convert_path: ``'convert'`` + Path to ImageMagick's convert binary. Unqualified paths are resolved by subprocess.Popen, except that on Windows, we look up an install of ImageMagick in the registry (as convert is also the name of a system tool). + +.. _rcparam_animation_convert_args: + +animation.convert_args: ``['-layers', 'OptimizePlus']`` + Additional arguments to pass to convert + +.. _rcparam_animation_embed_limit: + +animation.embed_limit: ``20.0`` + Limit, in MB, of size of base64 encoded animation in HTML (i.e. IPython notebook) + +.. _rcparam__internal_classic_mode: + +_internal.classic_mode: ``False`` + *no description* + +.. _rcparam_backend: + +backend: ``None`` + *no description* diff --git a/doc/users/faq.rst b/doc/users/faq.rst index c6bbc5ca8d87..5aec1e08fb14 100644 --- a/doc/users/faq.rst +++ b/doc/users/faq.rst @@ -77,8 +77,8 @@ empty if it was rendered pure white (there may be artists present, but they could be outside the drawing area or transparent)? For the purpose here, we define empty as: "The figure does not contain any -artists except it's background patch." The exception for the background is -necessary, because by default every figure contains a `.Rectangle` as it's +artists except its background patch." The exception for the background is +necessary, because by default every figure contains a `.Rectangle` as its background patch. This definition could be checked via:: def is_empty(figure): @@ -91,8 +91,8 @@ background patch. This definition could be checked via:: We've decided not to include this as a figure method because this is only one way of defining empty, and checking the above is only rarely necessary. -Usually the user or program handling the figure know if they have added -something to the figure. +Whether or not something has been added to the figure is usually defined +within the context of the program. The only reliable way to check whether a figure would render empty is to actually perform such a rendering and inspect the result. @@ -281,7 +281,7 @@ locators as desired because the two axes are independent. Generate images without having a window appear ---------------------------------------------- -The recommended approach since matplotlib 3.1 is to explicitly create a Figure +The recommended approach since Matplotlib 3.1 is to explicitly create a Figure instance:: from matplotlib.figure import Figure @@ -292,12 +292,10 @@ instance:: This prevents any interaction with GUI frameworks and the window manager. -It's alternatively still possible to use the pyplot interface. Instead of -calling `matplotlib.pyplot.show`, call `matplotlib.pyplot.savefig`. - -Additionally, you must ensure to close the figure after saving it. Not -closing the figure is a memory leak, because pyplot keeps references -to all not-yet-shown figures:: +It's alternatively still possible to use the pyplot interface: instead of +calling `matplotlib.pyplot.show`, call `matplotlib.pyplot.savefig`. In that +case, you must close the figure after saving it. Not closing the figure causes +a memory leak, because pyplot keeps references to all not-yet-shown figures. :: import matplotlib.pyplot as plt plt.plot([1, 2, 3]) @@ -367,7 +365,7 @@ provide the following information in your e-mail to the `mailing list * Matplotlib provides debugging information through the `logging` library, and a helper function to set the logging level: one can call :: - plt.set_loglevel("info") # or "debug" for more info + plt.set_loglevel("INFO") # or "DEBUG" for more info to obtain this debugging information. diff --git a/doc/users/getting_started/index.rst b/doc/users/getting_started/index.rst index ac896687979d..dfbbd615b5cd 100644 --- a/doc/users/getting_started/index.rst +++ b/doc/users/getting_started/index.rst @@ -4,26 +4,7 @@ Getting started Installation quick-start ------------------------ -.. grid:: 1 1 2 2 - - .. grid-item:: - - Install using `pip `__: - - .. code-block:: bash - - pip install matplotlib - - .. grid-item:: - - Install using `conda `__: - - .. code-block:: bash - - conda install -c conda-forge matplotlib - -Further details are available in the :doc:`Installation Guide `. - +.. include:: /install/quick_install.inc.rst Draw a first plot ----------------- diff --git a/doc/users/glossary.rst b/doc/users/glossary.rst new file mode 100644 index 000000000000..8a2a3fd96bd1 --- /dev/null +++ b/doc/users/glossary.rst @@ -0,0 +1,44 @@ +======== +Glossary +======== + +.. Note for glossary authors: + The glossary is primarily intended for Matplotlib's own concepts and + terminology, e.g. figure, artist, backend, etc. We don't want to list + general terms like "GUI framework", "event loop" or similar. + The glossary should contain a short definition of the term, aiming at + a high-level understanding. Use links to redirect to more comprehensive + explanations and API reference when possible. + +This glossary defines concepts and terminology specific to Matplotlib. + +.. glossary:: + + Figure + The outermost container for a Matplotlib graphic. Think of this as the + canvas to draw on. + + This is implemented in the class `.Figure`. For more details see + :ref:`figure-intro`. + + Axes + This is a container for what is often colloquially called a plot/chart/graph. + It's a data area with :term:`Axis`\ es, i.e. coordinate directions, + and includes data artists like lines, bars etc. as well as + decorations like title, axis labels, legend. + + Since most "plotting operations" are realized as methods on `~.axes.Axes` + this is the object users will mostly interact with. + + Note: The term *Axes* was taken over from MATLAB. Think of this as + a container spanned by the *x*- and *y*-axis, including decoration + and data. + + Axis + A direction with a scale. The scale defines the mapping from + data coordinates to screen coordinates. The Axis also includes + the ticks and axis label. + + Artist + The base class for all graphical element that can be drawn. + Examples are Lines, Rectangles, Text, Ticks, Legend, Axes, ... diff --git a/doc/users/index.rst b/doc/users/index.rst index 2991e7d2b324..b98bda824a7e 100644 --- a/doc/users/index.rst +++ b/doc/users/index.rst @@ -59,6 +59,7 @@ Using Matplotlib :maxdepth: 2 :includehidden: + explain/configuration explain/customizing .. grid-item-card:: @@ -103,3 +104,4 @@ Using Matplotlib getting_started/index ../install/index + glossary diff --git a/environment.yml b/environment.yml index eaa6ed6386b6..418062d0d237 100644 --- a/environment.yml +++ b/environment.yml @@ -37,7 +37,7 @@ dependencies: - ipywidgets - numpydoc>=1.0 - packaging>=20 - - pydata-sphinx-theme~=0.15.0 + - pydata-sphinx-theme=0.16.1 # required by mpl-sphinx-theme=3.10 - pyyaml - sphinx>=3.0.0,!=6.1.2 - sphinx-copybutton @@ -46,14 +46,14 @@ dependencies: - sphinx-design - sphinx-tags>=0.4.0 - pystemmer + - pikepdf - pip - pip: - - mpl-sphinx-theme~=3.8.0 + - mpl-sphinx-theme~=3.10.0 - sphinxcontrib-svg2pdfconverter>=1.1.0 - sphinxcontrib-video>=0.2.1 - - pikepdf # testing - - black<24 + - black<26 - coverage - gtk4 - ipykernel diff --git a/extern/agg24-svn/include/agg_image_filters.h b/extern/agg24-svn/include/agg_image_filters.h index 8e1bc8f0dba4..e5b813dfc8a6 100644 --- a/extern/agg24-svn/include/agg_image_filters.h +++ b/extern/agg24-svn/include/agg_image_filters.h @@ -53,8 +53,13 @@ namespace agg double r = filter.radius(); realloc_lut(r); unsigned i; +#ifndef MPL_FIX_AGG_IMAGE_FILTER_LUT_BUGS unsigned pivot = diameter() << (image_subpixel_shift - 1); for(i = 0; i < pivot; i++) +#else + unsigned pivot = (diameter() << (image_subpixel_shift - 1)) - 1; + for(i = 0; i < pivot + 1; i++) +#endif { double x = double(i) / double(image_subpixel_scale); double y = filter.calc_weight(x); @@ -62,7 +67,11 @@ namespace agg m_weight_array[pivot - i] = (int16)iround(y * image_filter_scale); } unsigned end = (diameter() << image_subpixel_shift) - 1; +#ifndef MPL_FIX_AGG_IMAGE_FILTER_LUT_BUGS m_weight_array[0] = m_weight_array[end]; +#else + m_weight_array[end] = (int16)iround(filter.calc_weight(diameter() / 2) * image_filter_scale); +#endif if(normalization) { normalize(); diff --git a/extern/agg24-svn/include/agg_span_interpolator_linear.h b/extern/agg24-svn/include/agg_span_interpolator_linear.h index ef10505ce11a..39cc7610b00c 100644 --- a/extern/agg24-svn/include/agg_span_interpolator_linear.h +++ b/extern/agg24-svn/include/agg_span_interpolator_linear.h @@ -53,6 +53,10 @@ namespace agg //---------------------------------------------------------------- void begin(double x, double y, unsigned len) { +#ifdef MPL_FIX_AGG_INTERPOLATION_ENDPOINT_BUG + len -= 1; +#endif + double tx; double ty; @@ -75,6 +79,10 @@ namespace agg //---------------------------------------------------------------- void resynchronize(double xe, double ye, unsigned len) { +#ifdef MPL_FIX_AGG_INTERPOLATION_ENDPOINT_BUG + len -= 1; +#endif + m_trans->transform(&xe, &ye); m_li_x = dda2_line_interpolator(m_li_x.y(), iround(xe * subpixel_scale), len); m_li_y = dda2_line_interpolator(m_li_y.y(), iround(ye * subpixel_scale), len); diff --git a/extern/agg24-svn/include/ctrl/agg_gamma_spline.h b/extern/agg24-svn/include/ctrl/agg_gamma_spline.h index 4f21710d9f29..052f972e85c1 100644 --- a/extern/agg24-svn/include/ctrl/agg_gamma_spline.h +++ b/extern/agg24-svn/include/ctrl/agg_gamma_spline.h @@ -56,7 +56,7 @@ namespace agg // bounding rectangle. Function values() calculates the curve by these // 4 values. After calling it one can get the gamma-array with call gamma(). // Class also supports the vertex source interface, i.e rewind() and - // vertex(). It's made for convinience and used in class gamma_ctrl. + // vertex(). It's made for convenience and used in class gamma_ctrl. // Before calling rewind/vertex one must set the bounding box // box() using pixel coordinates. //------------------------------------------------------------------------ diff --git a/extern/agg24-svn/src/agg_image_filters.cpp b/extern/agg24-svn/src/agg_image_filters.cpp index 549d9adbf5af..1571308be55a 100644 --- a/extern/agg24-svn/src/agg_image_filters.cpp +++ b/extern/agg24-svn/src/agg_image_filters.cpp @@ -88,6 +88,7 @@ namespace agg } } +#ifndef MPL_FIX_AGG_IMAGE_FILTER_LUT_BUGS unsigned pivot = m_diameter << (image_subpixel_shift - 1); for(i = 0; i < pivot; i++) @@ -96,6 +97,7 @@ namespace agg } unsigned end = (diameter() << image_subpixel_shift) - 1; m_weight_array[0] = m_weight_array[end]; +#endif } diff --git a/galleries/examples/axes_grid1/demo_colorbar_with_inset_locator.py b/galleries/examples/axes_grid1/demo_colorbar_with_inset_locator.py index f62a0f58e3bc..352c8527910e 100644 --- a/galleries/examples/axes_grid1/demo_colorbar_with_inset_locator.py +++ b/galleries/examples/axes_grid1/demo_colorbar_with_inset_locator.py @@ -1,17 +1,17 @@ """ .. _demo-colorbar-with-inset-locator: -=========================================================== -Control the position and size of a colorbar with Inset Axes -=========================================================== +========================================================================= +Control the position and size of a colorbar with inset_locator.inset_axes +========================================================================= This example shows how to control the position, height, and width of colorbars -using `~mpl_toolkits.axes_grid1.inset_locator.inset_axes`. +using `.inset_locator.inset_axes`. -Inset Axes placement is controlled as for legends: either by providing a *loc* -option ("upper right", "best", ...), or by providing a locator with respect to -the parent bbox. Parameters such as *bbox_to_anchor* and *borderpad* likewise -work in the same way, and are also demonstrated here. +`.inset_locator.inset_axes` placement is controlled as for legends: either +by providing a *loc* option ("upper right", "best", ...), or by providing a +locator with respect to the parent bbox. Parameters such as *bbox_to_anchor* +and *borderpad* likewise work in the same way, and are also demonstrated here. Users should consider using `.Axes.inset_axes` instead (see :ref:`colorbar_placement`). @@ -21,12 +21,12 @@ import matplotlib.pyplot as plt -from mpl_toolkits.axes_grid1.inset_locator import inset_axes +from mpl_toolkits.axes_grid1 import inset_locator fig, (ax1, ax2) = plt.subplots(1, 2, figsize=[6, 3]) im1 = ax1.imshow([[1, 2], [2, 3]]) -axins1 = inset_axes( +axins1 = inset_locator.inset_axes( ax1, width="50%", # width: 50% of parent_bbox width height="5%", # height: 5% @@ -36,7 +36,7 @@ fig.colorbar(im1, cax=axins1, orientation="horizontal", ticks=[1, 2, 3]) im = ax2.imshow([[1, 2], [2, 3]]) -axins = inset_axes( +axins = inset_locator.inset_axes( ax2, width="5%", # width: 5% of parent_bbox width height="50%", # height: 50% diff --git a/galleries/examples/axisartist/demo_axis_direction.py b/galleries/examples/axisartist/demo_axis_direction.py index 9540599c6a7b..6bc46fe273a0 100644 --- a/galleries/examples/axisartist/demo_axis_direction.py +++ b/galleries/examples/axisartist/demo_axis_direction.py @@ -10,20 +10,15 @@ from matplotlib.projections import PolarAxes from matplotlib.transforms import Affine2D import mpl_toolkits.axisartist as axisartist -import mpl_toolkits.axisartist.angle_helper as angle_helper -import mpl_toolkits.axisartist.grid_finder as grid_finder -from mpl_toolkits.axisartist.grid_helper_curvelinear import \ - GridHelperCurveLinear +from mpl_toolkits.axisartist import angle_helper, grid_finder +from mpl_toolkits.axisartist.grid_helper_curvelinear import GridHelperCurveLinear def setup_axes(fig, rect): """Polar projection, but in a rectangular box.""" # see demo_curvelinear_grid.py for details grid_helper = GridHelperCurveLinear( - ( - Affine2D().scale(np.pi/180., 1.) + - PolarAxes.PolarTransform() - ), + Affine2D().scale(np.pi/180., 1.) + PolarAxes.PolarTransform(), extreme_finder=angle_helper.ExtremeFinderCycle( 20, 20, lon_cycle=360, lat_cycle=None, diff --git a/galleries/examples/axisartist/demo_curvelinear_grid.py b/galleries/examples/axisartist/demo_curvelinear_grid.py index fb1fbdd011ce..83fc1ce0ceaa 100644 --- a/galleries/examples/axisartist/demo_curvelinear_grid.py +++ b/galleries/examples/axisartist/demo_curvelinear_grid.py @@ -17,8 +17,7 @@ from matplotlib.projections import PolarAxes from matplotlib.transforms import Affine2D from mpl_toolkits.axisartist import Axes, HostAxes, angle_helper -from mpl_toolkits.axisartist.grid_helper_curvelinear import \ - GridHelperCurveLinear +from mpl_toolkits.axisartist.grid_helper_curvelinear import GridHelperCurveLinear def curvelinear_test1(fig): diff --git a/galleries/examples/axisartist/demo_curvelinear_grid2.py b/galleries/examples/axisartist/demo_curvelinear_grid2.py index d4ac36cc717b..a3cd06ef6706 100644 --- a/galleries/examples/axisartist/demo_curvelinear_grid2.py +++ b/galleries/examples/axisartist/demo_curvelinear_grid2.py @@ -14,10 +14,8 @@ import numpy as np from mpl_toolkits.axisartist.axislines import Axes -from mpl_toolkits.axisartist.grid_finder import (ExtremeFinderSimple, - MaxNLocator) -from mpl_toolkits.axisartist.grid_helper_curvelinear import \ - GridHelperCurveLinear +from mpl_toolkits.axisartist.grid_finder import ExtremeFinderSimple, MaxNLocator +from mpl_toolkits.axisartist.grid_helper_curvelinear import GridHelperCurveLinear def curvelinear_test1(fig): diff --git a/galleries/examples/axisartist/demo_floating_axes.py b/galleries/examples/axisartist/demo_floating_axes.py index add03e266d3e..d36b534161c1 100644 --- a/galleries/examples/axisartist/demo_floating_axes.py +++ b/galleries/examples/axisartist/demo_floating_axes.py @@ -22,8 +22,7 @@ from matplotlib.transforms import Affine2D import mpl_toolkits.axisartist.angle_helper as angle_helper import mpl_toolkits.axisartist.floating_axes as floating_axes -from mpl_toolkits.axisartist.grid_finder import (DictFormatter, FixedLocator, - MaxNLocator) +from mpl_toolkits.axisartist.grid_finder import DictFormatter, FixedLocator, MaxNLocator # Fixing random state for reproducibility np.random.seed(19680801) @@ -57,21 +56,18 @@ def setup_axes2(fig, rect): tr = PolarAxes.PolarTransform() pi = np.pi - angle_ticks = [(0, r"$0$"), - (.25*pi, r"$\frac{1}{4}\pi$"), - (.5*pi, r"$\frac{1}{2}\pi$")] - grid_locator1 = FixedLocator([v for v, s in angle_ticks]) - tick_formatter1 = DictFormatter(dict(angle_ticks)) - - grid_locator2 = MaxNLocator(2) - + angle_ticks = { + 0: r"$0$", + pi/4: r"$\frac{1}{4}\pi$", + pi/2: r"$\frac{1}{2}\pi$", + } grid_helper = floating_axes.GridHelperCurveLinear( tr, extremes=(.5*pi, 0, 2, 1), - grid_locator1=grid_locator1, - grid_locator2=grid_locator2, - tick_formatter1=tick_formatter1, - tick_formatter2=None) - + grid_locator1=FixedLocator([*angle_ticks]), + tick_formatter1=DictFormatter(angle_ticks), + grid_locator2=MaxNLocator(2), + tick_formatter2=None, + ) ax1 = fig.add_subplot( rect, axes_class=floating_axes.FloatingAxes, grid_helper=grid_helper) ax1.grid() @@ -93,30 +89,22 @@ def setup_axes3(fig, rect): Sometimes, things like axis_direction need to be adjusted. """ - # rotate a bit for better orientation - tr_rotate = Affine2D().translate(-95, 0) - - # scale degree to radians - tr_scale = Affine2D().scale(np.pi/180., 1.) - + tr_rotate = Affine2D().translate(-95, 0) # rotate a bit for better orientation + tr_scale = Affine2D().scale(np.pi/180., 1.) # scale degree to radians tr = tr_rotate + tr_scale + PolarAxes.PolarTransform() - grid_locator1 = angle_helper.LocatorHMS(4) - tick_formatter1 = angle_helper.FormatterHMS() - - grid_locator2 = MaxNLocator(3) - # Specify theta limits in degrees ra0, ra1 = 8.*15, 14.*15 # Specify radial limits cz0, cz1 = 0, 14000 + grid_helper = floating_axes.GridHelperCurveLinear( tr, extremes=(ra0, ra1, cz0, cz1), - grid_locator1=grid_locator1, - grid_locator2=grid_locator2, - tick_formatter1=tick_formatter1, - tick_formatter2=None) - + grid_locator1=angle_helper.LocatorHMS(4), + tick_formatter1=angle_helper.FormatterHMS(), + grid_locator2=MaxNLocator(3), + tick_formatter2=None, + ) ax1 = fig.add_subplot( rect, axes_class=floating_axes.FloatingAxes, grid_helper=grid_helper) diff --git a/galleries/examples/axisartist/demo_floating_axis.py b/galleries/examples/axisartist/demo_floating_axis.py index 0894bf8f4ce1..7760ed2089be 100644 --- a/galleries/examples/axisartist/demo_floating_axis.py +++ b/galleries/examples/axisartist/demo_floating_axis.py @@ -22,29 +22,19 @@ def curvelinear_test2(fig): """Polar projection, but in a rectangular box.""" # see demo_curvelinear_grid.py for details - tr = Affine2D().scale(np.pi / 180., 1.) + PolarAxes.PolarTransform() - - extreme_finder = angle_helper.ExtremeFinderCycle(20, - 20, - lon_cycle=360, - lat_cycle=None, - lon_minmax=None, - lat_minmax=(0, np.inf), - ) - - grid_locator1 = angle_helper.LocatorDMS(12) - - tick_formatter1 = angle_helper.FormatterDMS() - - grid_helper = GridHelperCurveLinear(tr, - extreme_finder=extreme_finder, - grid_locator1=grid_locator1, - tick_formatter1=tick_formatter1 - ) - + grid_helper = GridHelperCurveLinear( + Affine2D().scale(np.pi / 180., 1.) + PolarAxes.PolarTransform(), + extreme_finder=angle_helper.ExtremeFinderCycle( + 20, 20, + lon_cycle=360, lat_cycle=None, + lon_minmax=None, lat_minmax=(0, np.inf), + ), + grid_locator1=angle_helper.LocatorDMS(12), + tick_formatter1=angle_helper.FormatterDMS(), + ) ax1 = fig.add_subplot(axes_class=HostAxes, grid_helper=grid_helper) - # Now creates floating axis + # Now create floating axis # floating axis whose first coordinate (theta) is fixed at 60 ax1.axis["lat"] = axis = ax1.new_floating_axis(0, 60) diff --git a/galleries/examples/axisartist/simple_axis_pad.py b/galleries/examples/axisartist/simple_axis_pad.py index f40a1aa9f273..f436ae3ab79c 100644 --- a/galleries/examples/axisartist/simple_axis_pad.py +++ b/galleries/examples/axisartist/simple_axis_pad.py @@ -11,45 +11,29 @@ from matplotlib.projections import PolarAxes from matplotlib.transforms import Affine2D import mpl_toolkits.axisartist as axisartist -import mpl_toolkits.axisartist.angle_helper as angle_helper -import mpl_toolkits.axisartist.grid_finder as grid_finder -from mpl_toolkits.axisartist.grid_helper_curvelinear import \ - GridHelperCurveLinear +from mpl_toolkits.axisartist import angle_helper, grid_finder +from mpl_toolkits.axisartist.grid_helper_curvelinear import GridHelperCurveLinear def setup_axes(fig, rect): """Polar projection, but in a rectangular box.""" - # see demo_curvelinear_grid.py for details - tr = Affine2D().scale(np.pi/180., 1.) + PolarAxes.PolarTransform() - - extreme_finder = angle_helper.ExtremeFinderCycle(20, 20, - lon_cycle=360, - lat_cycle=None, - lon_minmax=None, - lat_minmax=(0, np.inf), - ) - - grid_locator1 = angle_helper.LocatorDMS(12) - grid_locator2 = grid_finder.MaxNLocator(5) - - tick_formatter1 = angle_helper.FormatterDMS() - - grid_helper = GridHelperCurveLinear(tr, - extreme_finder=extreme_finder, - grid_locator1=grid_locator1, - grid_locator2=grid_locator2, - tick_formatter1=tick_formatter1 - ) - - ax1 = fig.add_subplot( - rect, axes_class=axisartist.Axes, grid_helper=grid_helper) - ax1.axis[:].set_visible(False) - ax1.set_aspect(1.) - ax1.set_xlim(-5, 12) - ax1.set_ylim(-5, 10) - - return ax1 + grid_helper = GridHelperCurveLinear( + Affine2D().scale(np.pi/180., 1.) + PolarAxes.PolarTransform(), + extreme_finder=angle_helper.ExtremeFinderCycle( + 20, 20, + lon_cycle=360, lat_cycle=None, + lon_minmax=None, lat_minmax=(0, np.inf), + ), + grid_locator1=angle_helper.LocatorDMS(12), + grid_locator2=grid_finder.MaxNLocator(5), + tick_formatter1=angle_helper.FormatterDMS(), + ) + ax = fig.add_subplot( + rect, axes_class=axisartist.Axes, grid_helper=grid_helper, + aspect=1, xlim=(-5, 12), ylim=(-5, 10)) + ax.axis[:].set_visible(False) + return ax def add_floating_axis1(ax1): @@ -74,9 +58,6 @@ def add_floating_axis2(ax1): def ann(ax1, d): - if plt.rcParams["text.usetex"]: - d = d.replace("_", r"\_") - ax1.annotate(d, (0.5, 1), (5, -5), xycoords="axes fraction", textcoords="offset points", va="top", ha="center") diff --git a/galleries/examples/color/color_demo.py b/galleries/examples/color/color_demo.py index 6822efc3faa7..b8b06c9091e3 100644 --- a/galleries/examples/color/color_demo.py +++ b/galleries/examples/color/color_demo.py @@ -16,7 +16,7 @@ 5) a single letter string, i.e. one of ``{'b', 'g', 'r', 'c', 'm', 'y', 'k', 'w'}``, which are short-hand notations for shades of blue, green, red, cyan, magenta, yellow, black, and white; -6) a X11/CSS4 ("html") color name, e.g. ``"blue"``; +6) an X11/CSS4 ("html") color name, e.g. ``"blue"``; 7) a name from the `xkcd color survey `__, prefixed with ``'xkcd:'`` (e.g., ``'xkcd:sky blue'``); 8) a "Cn" color spec, i.e. ``'C'`` followed by a number, which is an index into diff --git a/galleries/examples/color/color_sequences.py b/galleries/examples/color/color_sequences.py index 4fc5571a0b69..4d7324b55a12 100644 --- a/galleries/examples/color/color_sequences.py +++ b/galleries/examples/color/color_sequences.py @@ -38,8 +38,8 @@ def plot_color_sequences(names, ax): built_in_color_sequences = [ 'tab10', 'tab20', 'tab20b', 'tab20c', 'Pastel1', 'Pastel2', 'Paired', - 'Accent', 'Dark2', 'Set1', 'Set2', 'Set3', 'petroff6', 'petroff8', - 'petroff10'] + 'Accent', 'okabe_ito', 'Dark2', 'Set1', 'Set2', 'Set3', 'petroff6', + 'petroff8', 'petroff10'] fig, ax = plt.subplots(figsize=(6.4, 9.6), layout='constrained') diff --git a/galleries/examples/color/colormap_reference.py b/galleries/examples/color/colormap_reference.py index 6f550161f2e9..eedf6ec11737 100644 --- a/galleries/examples/color/colormap_reference.py +++ b/galleries/examples/color/colormap_reference.py @@ -24,16 +24,15 @@ 'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu', 'GnBu', 'PuBu', 'YlGnBu', 'PuBuGn', 'BuGn', 'YlGn']), ('Sequential (2)', [ - 'binary', 'gist_yarg', 'gist_gray', 'gray', 'bone', 'pink', - 'spring', 'summer', 'autumn', 'winter', 'cool', 'Wistia', - 'hot', 'afmhot', 'gist_heat', 'copper']), + 'gray', 'bone', 'pink', 'spring', 'summer', 'autumn', 'winter', + 'cool', 'Wistia', 'hot', 'afmhot', 'gist_heat', 'copper']), ('Diverging', [ 'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', 'RdYlBu', 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic', 'berlin', 'managua', 'vanimo']), ('Cyclic', ['twilight', 'twilight_shifted', 'hsv']), ('Qualitative', [ - 'Pastel1', 'Pastel2', 'Paired', 'Accent', + 'Pastel1', 'Pastel2', 'Paired', 'Accent', 'okabe_ito', 'Dark2', 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', 'tab20c']), ('Miscellaneous', [ @@ -70,6 +69,22 @@ def plot_color_gradients(cmap_category, cmap_list): # %% +# +# .. admonition:: Discouraged +# +# For backward compatibility we additionally support the following colormap +# names, which are identical to other builtin colormaps. Their use is +# discouraged. Use the suggested replacement instead. +# +# ========= ================================= +# Colormap Use identical replacement instead +# ========= ================================= +# gist_gray gray +# gist_yarg gray_r +# binary gray_r +# ========= ================================= +# +# # .. _reverse-cmap: # # Reversed colormaps diff --git a/galleries/examples/images_contours_and_fields/trigradient_demo.py b/galleries/examples/images_contours_and_fields/trigradient_demo.py index aa3cbc889eba..dcfd23ada73b 100644 --- a/galleries/examples/images_contours_and_fields/trigradient_demo.py +++ b/galleries/examples/images_contours_and_fields/trigradient_demo.py @@ -9,8 +9,7 @@ import matplotlib.pyplot as plt import numpy as np -from matplotlib.tri import (CubicTriInterpolator, Triangulation, - UniformTriRefiner) +from matplotlib.tri import CubicTriInterpolator, Triangulation, UniformTriRefiner # ---------------------------------------------------------------------------- diff --git a/galleries/examples/lines_bars_and_markers/barh.py b/galleries/examples/lines_bars_and_markers/barh.py index 5493c7456c75..8529698c1ddb 100644 --- a/galleries/examples/lines_bars_and_markers/barh.py +++ b/galleries/examples/lines_bars_and_markers/barh.py @@ -6,22 +6,16 @@ This example showcases a simple horizontal bar chart. """ import matplotlib.pyplot as plt -import numpy as np - -# Fixing random state for reproducibility -np.random.seed(19680801) fig, ax = plt.subplots() # Example data people = ('Tom', 'Dick', 'Harry', 'Slim', 'Jim') -y_pos = np.arange(len(people)) -performance = 3 + 10 * np.random.rand(len(people)) -error = np.random.rand(len(people)) +performance = [5, 7, 6, 4, 9] +error = [0.2, 0.4, 0.3, 0.6, 0.2] -ax.barh(y_pos, performance, xerr=error, align='center') -ax.set_yticks(y_pos, labels=people) -ax.invert_yaxis() # labels read top-to-bottom +ax.barh(people, performance, xerr=error, align='center') +ax.yaxis.set_inverted(True) # arrange data from top to bottom ax.set_xlabel('Performance') ax.set_title('How fast do you want to go today?') diff --git a/galleries/examples/lines_bars_and_markers/broken_barh.py b/galleries/examples/lines_bars_and_markers/broken_barh.py index 3714ca7c748d..a709e911773d 100644 --- a/galleries/examples/lines_bars_and_markers/broken_barh.py +++ b/galleries/examples/lines_bars_and_markers/broken_barh.py @@ -18,13 +18,13 @@ network = np.column_stack([10*np.random.random(10), np.full(10, 0.05)]) fig, ax = plt.subplots() -# broken_barh(xranges, (ymin, height)) -ax.broken_barh(cpu_1, (-0.2, 0.4)) -ax.broken_barh(cpu_2, (0.8, 0.4)) -ax.broken_barh(cpu_3, (1.8, 0.4)) -ax.broken_barh(cpu_4, (2.8, 0.4)) -ax.broken_barh(disk, (3.8, 0.4), color="tab:orange") -ax.broken_barh(network, (4.8, 0.4), color="tab:green") +# broken_barh(xranges, (ypos, height)) +ax.broken_barh(cpu_1, (0, 0.4), align="center") +ax.broken_barh(cpu_2, (1, 0.4), align="center") +ax.broken_barh(cpu_3, (2, 0.4), align="center") +ax.broken_barh(cpu_4, (3, 0.4), align="center") +ax.broken_barh(disk, (4, 0.4), align="center", color="tab:orange") +ax.broken_barh(network, (5, 0.4), align="center", color="tab:green") ax.set_xlim(0, 10) ax.set_yticks(range(6), labels=["CPU 1", "CPU 2", "CPU 3", "CPU 4", "disk", "network"]) diff --git a/galleries/examples/lines_bars_and_markers/hat_graph.py b/galleries/examples/lines_bars_and_markers/hat_graph.py index 0fb611bc9262..25e5f0b1ead3 100644 --- a/galleries/examples/lines_bars_and_markers/hat_graph.py +++ b/galleries/examples/lines_bars_and_markers/hat_graph.py @@ -29,35 +29,28 @@ def hat_graph(ax, xlabels, values, group_labels): The group labels displayed in the legend. """ - def label_bars(heights, rects): - """Attach a text label on top of each bar.""" - for height, rect in zip(heights, rects): - ax.annotate(f'{height}', - xy=(rect.get_x() + rect.get_width() / 2, height), - xytext=(0, 4), # 4 points vertical offset. - textcoords='offset points', - ha='center', va='bottom') - values = np.asarray(values) - x = np.arange(values.shape[1]) - ax.set_xticks(x, labels=xlabels) - spacing = 0.3 # spacing between hat groups - width = (1 - spacing) / values.shape[0] - heights0 = values[0] - for i, (heights, group_label) in enumerate(zip(values, group_labels)): - style = {'fill': False} if i == 0 else {'edgecolor': 'black'} - rects = ax.bar(x - spacing/2 + i * width, heights - heights0, - width, bottom=heights0, label=group_label, **style) - label_bars(heights, rects) + color_cycle_colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] + + # Draw the hats + bars = ax.grouped_bar( + (values - values[0]).T, bottom=values[0], tick_labels=xlabels, + labels=group_labels, edgecolor='black', group_spacing=0.8, + colors=['none'] + color_cycle_colors) + # Attach a text label on top of each bar + for bc, heights in zip(bars.bar_containers, values): + ax.bar_label(bc, heights, padding=4) -# initialise labels and a numpy array make sure you have + +# Initialise labels and a numpy array make sure you have # N labels of N number of values in the array xlabels = ['I', 'II', 'III', 'IV', 'V'] playerA = np.array([5, 15, 22, 20, 25]) playerB = np.array([25, 32, 34, 30, 27]) -fig, ax = plt.subplots() +fig, ax = plt.subplots(layout='constrained') + hat_graph(ax, xlabels, [playerA, playerB], ['Player A', 'Player B']) # Add some text for labels, title and custom x-axis tick labels, etc. @@ -67,7 +60,6 @@ def label_bars(heights, rects): ax.set_title('Scores by number of game and players') ax.legend() -fig.tight_layout() plt.show() # %% # @@ -76,8 +68,8 @@ def label_bars(heights, rects): # The use of the following functions, methods, classes and modules is shown # in this example: # -# - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar` -# - `matplotlib.axes.Axes.annotate` / `matplotlib.pyplot.annotate` +# - `matplotlib.axes.Axes.grouped_bar` / `matplotlib.pyplot.grouped_bar` +# - `matplotlib.axes.Axes.bar_label` / `matplotlib.pyplot.bar_label` # # .. tags:: # diff --git a/galleries/examples/lines_bars_and_markers/multicolored_line.py b/galleries/examples/lines_bars_and_markers/multicolored_line.py index 3a71225d0112..a643b2de160c 100644 --- a/galleries/examples/lines_bars_and_markers/multicolored_line.py +++ b/galleries/examples/lines_bars_and_markers/multicolored_line.py @@ -72,7 +72,6 @@ def colored_line(x, y, c, ax=None, **lc_kwargs): # Plot the line collection to the axes ax = ax or plt.gca() ax.add_collection(lc) - ax.autoscale_view() return lc diff --git a/galleries/examples/misc/anchored_artists.py b/galleries/examples/misc/anchored_artists.py index bd1ec013c2a9..be600449bba6 100644 --- a/galleries/examples/misc/anchored_artists.py +++ b/galleries/examples/misc/anchored_artists.py @@ -17,8 +17,8 @@ from matplotlib import pyplot as plt from matplotlib.lines import Line2D -from matplotlib.offsetbox import (AnchoredOffsetbox, AuxTransformBox, - DrawingArea, TextArea, VPacker) +from matplotlib.offsetbox import (AnchoredOffsetbox, AuxTransformBox, DrawingArea, + TextArea, VPacker) from matplotlib.patches import Circle, Ellipse diff --git a/galleries/examples/misc/demo_agg_filter.py b/galleries/examples/misc/demo_agg_filter.py index 278fd998dd78..c736013e9718 100644 --- a/galleries/examples/misc/demo_agg_filter.py +++ b/galleries/examples/misc/demo_agg_filter.py @@ -269,19 +269,19 @@ def drop_shadow_patches(ax): def light_filter_pie(ax): fracs = [15, 30, 45, 10] explode = (0.1, 0.2, 0.1, 0.1) - pies = ax.pie(fracs, explode=explode) + pie = ax.pie(fracs, explode=explode) light_filter = LightFilter(9) - for p in pies[0]: + for p in pie.wedges: p.set_agg_filter(light_filter) p.set_rasterized(True) # to support mixed-mode renderers p.set(ec="none", lw=2) gauss = DropShadowFilter(9, offsets=(3, -4), alpha=0.7) - shadow = FilteredArtistList(pies[0], gauss) + shadow = FilteredArtistList(pie.wedges, gauss) ax.add_artist(shadow) - shadow.set_zorder(pies[0][0].get_zorder() - 0.1) + shadow.set_zorder(pie.wedges[0].get_zorder() - 0.1) if __name__ == "__main__": diff --git a/galleries/examples/misc/svg_filter_pie.py b/galleries/examples/misc/svg_filter_pie.py index b19867be9a2f..f8ccc5bcb22b 100644 --- a/galleries/examples/misc/svg_filter_pie.py +++ b/galleries/examples/misc/svg_filter_pie.py @@ -28,16 +28,16 @@ # We want to draw the shadow for each pie, but we will not use "shadow" # option as it doesn't save the references to the shadow patches. -pies = ax.pie(fracs, explode=explode, labels=labels, autopct='%1.1f%%') +pie = ax.pie(fracs, explode=explode, labels=labels, autopct='%1.1f%%') -for w in pies[0]: +for w in pie.wedges: # set the id with the label. w.set_gid(w.get_label()) # we don't want to draw the edge of the pie w.set_edgecolor("none") -for w in pies[0]: +for w in pie.wedges: # create shadow patch s = Shadow(w, -0.01, -0.01) s.set_gid(w.get_gid() + "_shadow") diff --git a/galleries/examples/mplot3d/intersecting_planes.py b/galleries/examples/mplot3d/intersecting_planes.py index a5a92caf5c6b..4f42e7bbeb94 100644 --- a/galleries/examples/mplot3d/intersecting_planes.py +++ b/galleries/examples/mplot3d/intersecting_planes.py @@ -12,7 +12,7 @@ example, we lift the problem of mutual overlap by segmenting the planes at their intersections, making four parts out of each plane. -This examples only works correctly for planes that cut each other in haves. This +This examples only works correctly for planes that cut each other in halves. This limitation is intentional to keep the code more readable. Cutting at arbitrary positions would of course be possible but makes the code even more complex. Thus, this example is more a demonstration of the concept how to work around diff --git a/galleries/examples/pie_and_polar_charts/bar_of_pie.py b/galleries/examples/pie_and_polar_charts/bar_of_pie.py index 6f18b964cef7..7c703976db2e 100644 --- a/galleries/examples/pie_and_polar_charts/bar_of_pie.py +++ b/galleries/examples/pie_and_polar_charts/bar_of_pie.py @@ -25,8 +25,8 @@ explode = [0.1, 0, 0] # rotate so that first wedge is split by the x-axis angle = -180 * overall_ratios[0] -wedges, *_ = ax1.pie(overall_ratios, autopct='%1.1f%%', startangle=angle, - labels=labels, explode=explode) +pie = ax1.pie(overall_ratios, autopct='%1.1f%%', startangle=angle, + labels=labels, explode=explode) # bar chart parameters age_ratios = [.33, .54, .07, .06] @@ -47,8 +47,8 @@ ax2.set_xlim(- 2.5 * width, 2.5 * width) # use ConnectionPatch to draw lines between the two plots -theta1, theta2 = wedges[0].theta1, wedges[0].theta2 -center, r = wedges[0].center, wedges[0].r +theta1, theta2 = pie.wedges[0].theta1, pie.wedges[0].theta2 +center, r = pie.wedges[0].center, pie.wedges[0].r bar_height = sum(age_ratios) # draw top connecting line diff --git a/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py b/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py index 13e3019bc7ba..78e884128d1e 100644 --- a/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py +++ b/galleries/examples/pie_and_polar_charts/pie_and_donut_labels.py @@ -6,7 +6,8 @@ Welcome to the Matplotlib bakery. We will create a pie and a donut chart through the `pie method ` and show how to label them with a `legend ` -as well as with `annotations `. +as well as with the `pie_label method ` and +`annotations `. """ # %% @@ -15,12 +16,14 @@ # Now it's time for the pie. Starting with a pie recipe, we create the data # and a list of labels from it. # -# We can provide a function to the ``autopct`` argument, which will expand -# automatic percentage labeling by showing absolute values; we calculate -# the latter back from relative data and the known sum of all values. +# We then create the pie and store the returned `~matplotlib.container.PieContainer` +# object for later. # -# We then create the pie and store the returned objects for later. The first -# returned element of the returned tuple is a list of the wedges. Those are +# We can provide the `~matplotlib.container.PieContainer` and a format string to +# the `~matplotlib.axes.Axes.pie_label` method to automatically label each +# ingredient's wedge with its weight in grams and percentages. +# +# The `~.PieContainer` has a list of patches as one of its attributes. Those are # `matplotlib.patches.Wedge` patches, which can directly be used as the handles # for a legend. We can use the legend's ``bbox_to_anchor`` argument to position # the legend outside of the pie. Here we use the axes coordinates ``(1, 0, 0.5, @@ -31,32 +34,26 @@ import matplotlib.pyplot as plt import numpy as np -fig, ax = plt.subplots(figsize=(6, 3), subplot_kw=dict(aspect="equal")) +fig, ax = plt.subplots(figsize=(6, 3)) recipe = ["375 g flour", "75 g sugar", "250 g butter", "300 g berries"] -data = [float(x.split()[0]) for x in recipe] +data = [int(x.split()[0]) for x in recipe] ingredients = [x.split()[-1] for x in recipe] +pie = ax.pie(data) -def func(pct, allvals): - absolute = int(np.round(pct/100.*np.sum(allvals))) - return f"{pct:.1f}%\n({absolute:d} g)" - +ax.pie_label(pie, '{frac:.1%}\n({absval:d}g)', + textprops=dict(color="w", size=8, weight="bold")) -wedges, texts, autotexts = ax.pie(data, autopct=lambda pct: func(pct, data), - textprops=dict(color="w")) - -ax.legend(wedges, ingredients, +ax.legend(pie.wedges, ingredients, title="Ingredients", loc="center left", bbox_to_anchor=(1, 0, 0.5, 1)) -plt.setp(autotexts, size=8, weight="bold") - ax.set_title("Matplotlib bakery: A pie") plt.show() @@ -97,13 +94,13 @@ def func(pct, allvals): data = [225, 90, 50, 60, 100, 5] -wedges, texts = ax.pie(data, wedgeprops=dict(width=0.5), startangle=-40) +pie = ax.pie(data, wedgeprops=dict(width=0.5), startangle=-40) bbox_props = dict(boxstyle="square,pad=0.3", fc="w", ec="k", lw=0.72) kw = dict(arrowprops=dict(arrowstyle="-"), bbox=bbox_props, zorder=0, va="center") -for i, p in enumerate(wedges): +for i, p in enumerate(pie.wedges): ang = (p.theta2 - p.theta1)/2. + p.theta1 y = np.sin(np.deg2rad(ang)) x = np.cos(np.deg2rad(ang)) @@ -131,6 +128,7 @@ def func(pct, allvals): # in this example: # # - `matplotlib.axes.Axes.pie` / `matplotlib.pyplot.pie` +# - `matplotlib.axes.Axes.pie_label` / `matplotlib.pyplot.pie_label` # - `matplotlib.axes.Axes.legend` / `matplotlib.pyplot.legend` # # .. tags:: diff --git a/galleries/examples/pie_and_polar_charts/pie_features.py b/galleries/examples/pie_and_polar_charts/pie_features.py index 47781a31a373..4c0eeaa4526e 100644 --- a/galleries/examples/pie_and_polar_charts/pie_features.py +++ b/galleries/examples/pie_and_polar_charts/pie_features.py @@ -108,7 +108,7 @@ fig, ax = plt.subplots() ax.pie(sizes, labels=labels, autopct='%.0f%%', - textprops={'size': 'smaller'}, radius=0.5) + textprops={'size': 'small'}, radius=0.5) plt.show() # %% diff --git a/galleries/examples/pie_and_polar_charts/pie_label.py b/galleries/examples/pie_and_polar_charts/pie_label.py new file mode 100644 index 000000000000..d7f690bd6f85 --- /dev/null +++ b/galleries/examples/pie_and_polar_charts/pie_label.py @@ -0,0 +1,100 @@ +""" +=================== +Labeling pie charts +=================== + +This example illustrates some features of the `~matplotlib.axes.Axes.pie_label` +method, which adds labels to an existing pie chart created with +`~matplotlib.axes.Axes.pie`. +""" + +# %% +# The simplest option is to provide a list of strings to label each slice of the pie. + +import matplotlib.pyplot as plt + +data = [36, 24, 8, 12] +labels = ['spam', 'eggs', 'bacon', 'sausage'] + +fig, ax = plt.subplots() +pie = ax.pie(data) +ax.pie_label(pie, labels) + +# %% +# +# If you want the labels outside the pie, set a *distance* greater than 1. +# This is the distance from the center of the pie as a fraction of its radius. + +fig, ax = plt.subplots() +pie = ax.pie(data) +ax.pie_label(pie, labels, distance=1.1) + +# %% +# +# You can also rotate the labels so they are oriented away from the pie center. + +fig, ax = plt.subplots() +pie = ax.pie(data) +ax.pie_label(pie, labels, rotate=True) + +# %% +# +# Instead of explicit labels, pass a format string to label slices with their values... + +fig, ax = plt.subplots() +pie = ax.pie(data) +ax.pie_label(pie, '{absval:.1f}') + +# %% +# +# ...or with their percentages... + +fig, ax = plt.subplots() +pie = ax.pie(data) +ax.pie_label(pie, '{frac:.1%}') + +# %% +# +# ...or both. + +fig, ax = plt.subplots() +pie = ax.pie(data) +ax.pie_label(pie, '{absval:d}\n{frac:.1%}') + +# %% +# +# Font styling can be configured by passing a dictionary to the *textprops* parameter. + +fig, ax = plt.subplots() +pie = ax.pie(data) +ax.pie_label(pie, labels, textprops={'fontsize': 'large', 'color': 'white'}) + +# %% +# +# `~matplotlib.axes.Axes.pie_label` can be called repeatedly to add multiple sets +# of labels. + +# sphinx_gallery_thumbnail_number = -1 + +fig, ax = plt.subplots() +pie = ax.pie(data) + +ax.pie_label(pie, labels, distance=1.1) +ax.pie_label(pie, '{frac:.1%}', distance=0.7) +ax.pie_label(pie, '{absval:d}', distance=0.4) + +plt.show() + +# %% +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.axes.Axes.pie` / `matplotlib.pyplot.pie` +# - `matplotlib.axes.Axes.pie_label` / `matplotlib.pyplot.pie_label` +# +# .. tags:: +# +# plot-type: pie +# level: beginner diff --git a/galleries/examples/shapes_and_collections/collections.py b/galleries/examples/shapes_and_collections/collections.py index 1f60afda1c5f..a5b25fd8d2bb 100644 --- a/galleries/examples/shapes_and_collections/collections.py +++ b/galleries/examples/shapes_and_collections/collections.py @@ -1,7 +1,7 @@ """ -========================================================= -Line, Poly and RegularPoly Collection with autoscaling -========================================================= +===================================== +Line, Poly and RegularPoly Collection +===================================== For the first two subplots, we will use spirals. Their size will be set in plot units, not data units. Their positions will be set in data units by using @@ -38,7 +38,7 @@ # Make some offsets xyo = rs.randn(npts, 2) -# Make a list of colors cycling through the default series. +# Make a list of colors from the default color cycle. colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2) @@ -47,57 +47,36 @@ col = collections.LineCollection( - [spiral], offsets=xyo, offset_transform=ax1.transData) + [spiral], offsets=xyo, offset_transform=ax1.transData, color=colors) +# transform the line segments such that their size is given in points trans = fig.dpi_scale_trans + transforms.Affine2D().scale(1.0/72.0) col.set_transform(trans) # the points to pixels transform -# Note: the first argument to the collection initializer -# must be a list of sequences of (x, y) tuples; we have only -# one sequence, but we still have to put it in a list. -ax1.add_collection(col, autolim=True) -# autolim=True enables autoscaling. For collections with -# offsets like this, it is neither efficient nor accurate, -# but it is good enough to generate a plot that you can use -# as a starting point. If you know beforehand the range of -# x and y that you want to show, it is better to set them -# explicitly, leave out the *autolim* keyword argument (or set it to False), -# and omit the 'ax1.autoscale_view()' call below. - -# Make a transform for the line segments such that their size is -# given in points: -col.set_color(colors) - -ax1.autoscale_view() # See comment above, after ax1.add_collection. +ax1.add_collection(col) ax1.set_title('LineCollection using offsets') # The same data as above, but fill the curves. col = collections.PolyCollection( - [spiral], offsets=xyo, offset_transform=ax2.transData) + [spiral], offsets=xyo, offset_transform=ax2.transData, color=colors) trans = transforms.Affine2D().scale(fig.dpi/72.0) col.set_transform(trans) # the points to pixels transform -ax2.add_collection(col, autolim=True) -col.set_color(colors) - - -ax2.autoscale_view() +ax2.add_collection(col) ax2.set_title('PolyCollection using offsets') -# 7-sided regular polygons +# 7-sided regular polygons col = collections.RegularPolyCollection( - 7, sizes=np.abs(xx) * 10.0, offsets=xyo, offset_transform=ax3.transData) + 7, sizes=np.abs(xx) * 10.0, offsets=xyo, offset_transform=ax3.transData, + color=colors) trans = transforms.Affine2D().scale(fig.dpi / 72.0) col.set_transform(trans) # the points to pixels transform -ax3.add_collection(col, autolim=True) -col.set_color(colors) -ax3.autoscale_view() +ax3.add_collection(col) ax3.set_title('RegularPolyCollection using offsets') # Simulate a series of ocean current profiles, successively # offset by 0.1 m/s so that they form what is sometimes called # a "waterfall" plot or a "stagger" plot. - nverts = 60 ncurves = 20 offs = (0.1, 0.0) @@ -111,16 +90,12 @@ curve = np.column_stack([xxx, yy * 100]) segs.append(curve) -col = collections.LineCollection(segs, offsets=offs) -ax4.add_collection(col, autolim=True) -col.set_color(colors) -ax4.autoscale_view() +col = collections.LineCollection(segs, offsets=offs, color=colors) +ax4.add_collection(col) ax4.set_title('Successive data offsets') ax4.set_xlabel('Zonal velocity component (m/s)') ax4.set_ylabel('Depth (m)') -# Reverse the y-axis so depth increases downward -ax4.set_ylim(ax4.get_ylim()[::-1]) - +ax4.invert_yaxis() # so that depth increases downward plt.show() @@ -136,6 +111,5 @@ # - `matplotlib.collections.LineCollection` # - `matplotlib.collections.RegularPolyCollection` # - `matplotlib.axes.Axes.add_collection` -# - `matplotlib.axes.Axes.autoscale_view` # - `matplotlib.transforms.Affine2D` # - `matplotlib.transforms.Affine2D.scale` diff --git a/galleries/examples/shapes_and_collections/donut.py b/galleries/examples/shapes_and_collections/donut.py index 8764101beb3e..3aa5b0b1a75c 100644 --- a/galleries/examples/shapes_and_collections/donut.py +++ b/galleries/examples/shapes_and_collections/donut.py @@ -15,27 +15,21 @@ def wise(v): - if v == 1: - return "CCW" - else: - return "CW" + return {+1: "CCW", -1: "CW"}[v] def make_circle(r): t = np.arange(0, np.pi * 2.0, 0.01) - t = t.reshape((len(t), 1)) x = r * np.cos(t) y = r * np.sin(t) - return np.hstack((x, y)) + return np.column_stack((x, y)) -Path = mpath.Path fig, ax = plt.subplots() inside_vertices = make_circle(0.5) outside_vertices = make_circle(1.0) -codes = np.ones( - len(inside_vertices), dtype=mpath.Path.code_type) * mpath.Path.LINETO +codes = np.full(len(inside_vertices), mpath.Path.LINETO) codes[0] = mpath.Path.MOVETO for i, (inside, outside) in enumerate(((1, 1), (1, -1), (-1, 1), (-1, -1))): @@ -44,23 +38,20 @@ def make_circle(r): vertices = np.concatenate((outside_vertices[::outside], inside_vertices[::inside])) # Shift the path - vertices[:, 0] += i * 2.5 + vertices += (i * 2.5, 0) # The codes will be all "LINETO" commands, except for "MOVETO"s at the # beginning of each subpath all_codes = np.concatenate((codes, codes)) # Create the Path object path = mpath.Path(vertices, all_codes) - # Add plot it + # And plot it patch = mpatches.PathPatch(path, facecolor='#885500', edgecolor='black') ax.add_patch(patch) ax.annotate(f"Outside {wise(outside)},\nInside {wise(inside)}", (i * 2.5, -1.5), va="top", ha="center") -ax.set_xlim(-2, 10) -ax.set_ylim(-3, 2) -ax.set_title('Mmm, donuts!') -ax.set_aspect(1.0) +ax.set(xlim=(-2, 10), ylim=(-3, 2), aspect=1, title="Mmm, donuts!") plt.show() # %% diff --git a/galleries/examples/shapes_and_collections/ellipse_collection.py b/galleries/examples/shapes_and_collections/ellipse_collection.py index 7118e5f7abf2..39f0cb7dcb6a 100644 --- a/galleries/examples/shapes_and_collections/ellipse_collection.py +++ b/galleries/examples/shapes_and_collections/ellipse_collection.py @@ -30,7 +30,6 @@ offset_transform=ax.transData) ec.set_array((X + Y).ravel()) ax.add_collection(ec) -ax.autoscale_view() ax.set_xlabel('X') ax.set_ylabel('y') cbar = plt.colorbar(ec) @@ -47,5 +46,4 @@ # - `matplotlib.collections` # - `matplotlib.collections.EllipseCollection` # - `matplotlib.axes.Axes.add_collection` -# - `matplotlib.axes.Axes.autoscale_view` # - `matplotlib.cm.ScalarMappable.set_array` diff --git a/galleries/examples/specialty_plots/skewt.py b/galleries/examples/specialty_plots/skewt.py index e25998a73c04..3a9c14ca6111 100644 --- a/galleries/examples/specialty_plots/skewt.py +++ b/galleries/examples/specialty_plots/skewt.py @@ -34,9 +34,9 @@ def draw(self, renderer): for artist in [self.gridline, self.tick1line, self.tick2line, self.label1, self.label2]: stack.callback(artist.set_visible, artist.get_visible()) - needs_lower = transforms.interval_contains( + needs_lower = transforms._interval_contains( self.axes.lower_xlim, self.get_loc()) - needs_upper = transforms.interval_contains( + needs_upper = transforms._interval_contains( self.axes.upper_xlim, self.get_loc()) self.tick1line.set_visible( self.tick1line.get_visible() and needs_lower) @@ -151,8 +151,7 @@ def upper_xlim(self): import matplotlib.pyplot as plt import numpy as np - from matplotlib.ticker import (MultipleLocator, NullFormatter, - ScalarFormatter) + from matplotlib.ticker import MultipleLocator, NullFormatter, ScalarFormatter # Some example data. data_txt = ''' diff --git a/galleries/examples/statistics/boxplot_vs_violin.py b/galleries/examples/statistics/boxplot_vs_violin.py index f277e737e65c..06aa2693f446 100644 --- a/galleries/examples/statistics/boxplot_vs_violin.py +++ b/galleries/examples/statistics/boxplot_vs_violin.py @@ -12,12 +12,15 @@ the whole range of the data. A good general reference on boxplots and their history can be found -here: http://vita.had.co.nz/papers/boxplots.pdf +here: https://vita.had.co.nz/papers/boxplots.pdf Violin plots require matplotlib >= 1.4. -For more information on violin plots, the scikit-learn docs have a great -section: https://scikit-learn.org/stable/modules/density.html +Violin plots show the distribution of the data as a rotated kernel density +estimate (KDE) along with summary statistics similar to a box plot. + +For more information on violin plots, see: +https://en.wikipedia.org/wiki/Violin_plot """ import matplotlib.pyplot as plt diff --git a/galleries/examples/lines_bars_and_markers/cohere.py b/galleries/examples/statistics/cohere.py similarity index 94% rename from galleries/examples/lines_bars_and_markers/cohere.py rename to galleries/examples/statistics/cohere.py index f02788ea1d69..4fd76d66a06a 100644 --- a/galleries/examples/lines_bars_and_markers/cohere.py +++ b/galleries/examples/statistics/cohere.py @@ -4,6 +4,8 @@ ===================================== An example showing how to plot the coherence of two signals using `~.Axes.cohere`. + +.. redirect-from:: /gallery/lines_bars_and_markers/cohere """ import matplotlib.pyplot as plt import numpy as np diff --git a/galleries/examples/lines_bars_and_markers/csd_demo.py b/galleries/examples/statistics/csd_demo.py similarity index 94% rename from galleries/examples/lines_bars_and_markers/csd_demo.py rename to galleries/examples/statistics/csd_demo.py index 76d9f0825223..31c657f6b47f 100644 --- a/galleries/examples/lines_bars_and_markers/csd_demo.py +++ b/galleries/examples/statistics/csd_demo.py @@ -4,6 +4,8 @@ ============================ Plot the cross spectral density (CSD) of two signals using `~.Axes.csd`. + +.. redirect-from:: /gallery/lines_bars_and_markers/csd_demo """ import matplotlib.pyplot as plt import numpy as np diff --git a/galleries/examples/lines_bars_and_markers/psd_demo.py b/galleries/examples/statistics/psd_demo.py similarity index 98% rename from galleries/examples/lines_bars_and_markers/psd_demo.py rename to galleries/examples/statistics/psd_demo.py index edbfc79289af..bf564df7542c 100644 --- a/galleries/examples/lines_bars_and_markers/psd_demo.py +++ b/galleries/examples/statistics/psd_demo.py @@ -8,6 +8,8 @@ The PSD is a common plot in the field of signal processing. NumPy has many useful libraries for computing a PSD. Below we demo a few examples of how this can be accomplished and visualized with Matplotlib. + +.. redirect-from:: /gallery/lines_bars_and_markers/psd_demo """ import matplotlib.pyplot as plt import numpy as np diff --git a/galleries/examples/lines_bars_and_markers/xcorr_acorr_demo.py b/galleries/examples/statistics/xcorr_acorr_demo.py similarity index 93% rename from galleries/examples/lines_bars_and_markers/xcorr_acorr_demo.py rename to galleries/examples/statistics/xcorr_acorr_demo.py index 7878ef8d7468..f0cd0ecaf8ed 100644 --- a/galleries/examples/lines_bars_and_markers/xcorr_acorr_demo.py +++ b/galleries/examples/statistics/xcorr_acorr_demo.py @@ -5,6 +5,8 @@ Example use of cross-correlation (`~.Axes.xcorr`) and auto-correlation (`~.Axes.acorr`) plots. + +.. redirect-from:: /gallery/lines_bars_and_markers/xcorr_acorr_demo """ import matplotlib.pyplot as plt import numpy as np diff --git a/galleries/examples/subplots_axes_and_figures/axes_zoom_effect.py b/galleries/examples/subplots_axes_and_figures/axes_zoom_effect.py index c8d09de45888..93c7662576e1 100644 --- a/galleries/examples/subplots_axes_and_figures/axes_zoom_effect.py +++ b/galleries/examples/subplots_axes_and_figures/axes_zoom_effect.py @@ -7,10 +7,8 @@ import matplotlib.pyplot as plt -from matplotlib.transforms import (Bbox, TransformedBbox, - blended_transform_factory) -from mpl_toolkits.axes_grid1.inset_locator import (BboxConnector, - BboxConnectorPatch, +from matplotlib.transforms import Bbox, TransformedBbox, blended_transform_factory +from mpl_toolkits.axes_grid1.inset_locator import (BboxConnector, BboxConnectorPatch, BboxPatch) diff --git a/galleries/examples/subplots_axes_and_figures/geo_demo.py b/galleries/examples/subplots_axes_and_figures/geo_demo.py index 256c440cc4d1..4c8d38cc8a52 100644 --- a/galleries/examples/subplots_axes_and_figures/geo_demo.py +++ b/galleries/examples/subplots_axes_and_figures/geo_demo.py @@ -6,7 +6,7 @@ This shows 4 possible geographic projections. Cartopy_ supports more projections. -.. _Cartopy: https://scitools.org.uk/cartopy/ +.. _Cartopy: https://cartopy.readthedocs.io """ import matplotlib.pyplot as plt diff --git a/galleries/examples/subplots_axes_and_figures/secondary_axis.py b/galleries/examples/subplots_axes_and_figures/secondary_axis.py index 842b296f78cf..146de1cceeca 100644 --- a/galleries/examples/subplots_axes_and_figures/secondary_axis.py +++ b/galleries/examples/subplots_axes_and_figures/secondary_axis.py @@ -9,6 +9,9 @@ `.axes.Axes.secondary_yaxis`. This secondary axis can have a different scale than the main axis by providing both a forward and an inverse conversion function in a tuple to the *functions* keyword argument: + +See also :doc:`/gallery/subplots_axes_and_figures/two_scales` for the case +where two scales are not related to one another, but independent. """ import datetime diff --git a/galleries/examples/subplots_axes_and_figures/two_scales.py b/galleries/examples/subplots_axes_and_figures/two_scales.py index 882fcac7866e..ea31f93c4251 100644 --- a/galleries/examples/subplots_axes_and_figures/two_scales.py +++ b/galleries/examples/subplots_axes_and_figures/two_scales.py @@ -12,7 +12,12 @@ Such Axes are generated by calling the `.Axes.twinx` method. Likewise, `.Axes.twiny` is available to generate Axes that share a *y* axis but have different top and bottom scales. + +See also :doc:`/gallery/subplots_axes_and_figures/secondary_axis` for the case +where the two scales are not independent, but related (e.g., the same quantity +in two different units). """ + import matplotlib.pyplot as plt import numpy as np diff --git a/galleries/examples/text_labels_and_annotations/annotate_transform.py b/galleries/examples/text_labels_and_annotations/annotate_transform.py deleted file mode 100644 index e7d4e11d9d38..000000000000 --- a/galleries/examples/text_labels_and_annotations/annotate_transform.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -================== -Annotate transform -================== - -This example shows how to use different coordinate systems for annotations. -For a complete overview of the annotation capabilities, also see the -:ref:`annotation tutorial`. - -.. redirect-from:: /gallery/pyplots/annotate_transform -""" - -import matplotlib.pyplot as plt -import numpy as np - -x = np.arange(0, 10, 0.005) -y = np.exp(-x/2.) * np.sin(2*np.pi*x) - -fig, ax = plt.subplots() -ax.plot(x, y) -ax.set_xlim(0, 10) -ax.set_ylim(-1, 1) - -xdata, ydata = 5, 0 -xdisplay, ydisplay = ax.transData.transform((xdata, ydata)) - -bbox = dict(boxstyle="round", fc="0.8") -arrowprops = dict( - arrowstyle="->", - connectionstyle="angle,angleA=0,angleB=90,rad=10") - -offset = 72 -ax.annotate( - f'data = ({xdata:.1f}, {ydata:.1f})', - (xdata, ydata), - xytext=(-2*offset, offset), textcoords='offset points', - bbox=bbox, arrowprops=arrowprops) -ax.annotate( - f'display = ({xdisplay:.1f}, {ydisplay:.1f})', - xy=(xdisplay, ydisplay), xycoords='figure pixels', - xytext=(0.5*offset, -offset), textcoords='offset points', - bbox=bbox, arrowprops=arrowprops) - -plt.show() - -# %% -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.transforms.Transform.transform` -# - `matplotlib.axes.Axes.annotate` / `matplotlib.pyplot.annotate` diff --git a/galleries/examples/text_labels_and_annotations/demo_annotation_box.py b/galleries/examples/text_labels_and_annotations/demo_annotation_box.py index ad28c4abd96c..e6c21bd69107 100644 --- a/galleries/examples/text_labels_and_annotations/demo_annotation_box.py +++ b/galleries/examples/text_labels_and_annotations/demo_annotation_box.py @@ -13,8 +13,7 @@ import numpy as np from matplotlib.cbook import get_sample_data -from matplotlib.offsetbox import (AnnotationBbox, DrawingArea, OffsetImage, - TextArea) +from matplotlib.offsetbox import AnnotationBbox, DrawingArea, OffsetImage, TextArea from matplotlib.patches import Circle fig, ax = plt.subplots() diff --git a/galleries/examples/text_labels_and_annotations/demo_text_path.py b/galleries/examples/text_labels_and_annotations/demo_text_path.py index bb4bbc628caa..ae79ff937093 100644 --- a/galleries/examples/text_labels_and_annotations/demo_text_path.py +++ b/galleries/examples/text_labels_and_annotations/demo_text_path.py @@ -13,8 +13,7 @@ from matplotlib.cbook import get_sample_data from matplotlib.image import BboxImage -from matplotlib.offsetbox import (AnchoredOffsetbox, AnnotationBbox, - AuxTransformBox) +from matplotlib.offsetbox import AnchoredOffsetbox, AnnotationBbox, AuxTransformBox from matplotlib.patches import PathPatch, Shadow from matplotlib.text import TextPath from matplotlib.transforms import IdentityTransform diff --git a/galleries/examples/text_labels_and_annotations/multiline.py b/galleries/examples/text_labels_and_annotations/multiline.py index 2aa6fea8c1af..e9ce81fe6526 100644 --- a/galleries/examples/text_labels_and_annotations/multiline.py +++ b/galleries/examples/text_labels_and_annotations/multiline.py @@ -11,7 +11,7 @@ ax0.set_aspect(1) ax0.plot(np.arange(10)) -ax0.set_xlabel('this is a xlabel\n(with newlines!)') +ax0.set_xlabel('this is an xlabel\n(with newlines!)') ax0.set_ylabel('this is vertical\ntest', multialignment='center') ax0.text(2, 7, 'this is\nyet another test', rotation=45, diff --git a/galleries/examples/ticks/align_ticklabels.py b/galleries/examples/ticks/align_ticklabels.py new file mode 100644 index 000000000000..ec36e0db4d07 --- /dev/null +++ b/galleries/examples/ticks/align_ticklabels.py @@ -0,0 +1,32 @@ +""" +================= +Align tick labels +================= + +By default, tick labels are aligned towards the axis. This means the set of +*y* tick labels appear right-aligned. Because the alignment reference point +is on the axis, left-aligned tick labels would overlap the plotting area. +To achieve a good-looking left-alignment, you have to additionally increase +the padding. +""" +import matplotlib.pyplot as plt + +population = { + "Sydney": 5.2, + "Mexico City": 8.8, + "São Paulo": 12.2, + "Istanbul": 15.9, + "Lagos": 15.9, + "Shanghai": 21.9, +} + +fig, ax = plt.subplots(layout="constrained") +ax.barh(population.keys(), population.values()) +ax.set_xlabel('Population (in millions)') + +# left-align all ticklabels +for ticklabel in ax.get_yticklabels(): + ticklabel.set_horizontalalignment("left") + +# increase padding +ax.tick_params("y", pad=70) diff --git a/galleries/examples/ticks/date_demo_rrule.py b/galleries/examples/ticks/date_demo_rrule.py index eb1fb605640d..948abde7584d 100644 --- a/galleries/examples/ticks/date_demo_rrule.py +++ b/galleries/examples/ticks/date_demo_rrule.py @@ -17,8 +17,7 @@ import matplotlib.pyplot as plt import numpy as np -from matplotlib.dates import (YEARLY, DateFormatter, RRuleLocator, drange, - rrulewrapper) +from matplotlib.dates import YEARLY, DateFormatter, RRuleLocator, drange, rrulewrapper # Fixing random state for reproducibility np.random.seed(19680801) diff --git a/galleries/examples/ticks/date_formatters_locators.py b/galleries/examples/ticks/date_formatters_locators.py index 8c3b24bb4c26..8d4922931323 100644 --- a/galleries/examples/ticks/date_formatters_locators.py +++ b/galleries/examples/ticks/date_formatters_locators.py @@ -13,11 +13,10 @@ import numpy as np # While these appear unused directly, they are used from eval'd strings. -from matplotlib.dates import (FR, MO, MONTHLY, SA, SU, TH, TU, WE, - AutoDateFormatter, AutoDateLocator, - ConciseDateFormatter, DateFormatter, DayLocator, - HourLocator, MicrosecondLocator, MinuteLocator, - MonthLocator, RRuleLocator, SecondLocator, +from matplotlib.dates import (FR, MO, MONTHLY, SA, SU, TH, TU, WE, AutoDateFormatter, + AutoDateLocator, ConciseDateFormatter, DateFormatter, + DayLocator, HourLocator, MicrosecondLocator, + MinuteLocator, MonthLocator, RRuleLocator, SecondLocator, WeekdayLocator, YearLocator, rrulewrapper) import matplotlib.ticker as ticker diff --git a/galleries/examples/user_interfaces/canvasagg.py b/galleries/examples/user_interfaces/canvasagg.py index 0e460cc64539..2786a2518dd3 100644 --- a/galleries/examples/user_interfaces/canvasagg.py +++ b/galleries/examples/user_interfaces/canvasagg.py @@ -32,10 +32,6 @@ from matplotlib.figure import Figure fig = Figure(figsize=(5, 4), dpi=100) -# A canvas must be manually attached to the figure (pyplot would automatically -# do it). This is done by instantiating the canvas with the figure as -# argument. -canvas = FigureCanvasAgg(fig) # Do some plotting. ax = fig.add_subplot() @@ -45,8 +41,12 @@ # etc.). fig.savefig("test.png") -# Option 2: Retrieve a memoryview on the renderer buffer, and convert it to a +# Option 2 (low-level approach to directly save to a numpy array): Manually +# attach a canvas to the figure (pyplot or savefig would automatically do +# it), by instantiating the canvas with the figure as argument; then draw the +# figure, retrieve a memoryview on the renderer buffer, and convert it to a # numpy array. +canvas = FigureCanvasAgg(fig) canvas.draw() rgba = np.asarray(canvas.buffer_rgba()) # ... and pass it to PIL. diff --git a/galleries/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py b/galleries/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py index 7c3b04041009..c5e3279b031d 100644 --- a/galleries/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py @@ -13,10 +13,8 @@ import numpy as np -from matplotlib.backends.backend_gtk3 import \ - NavigationToolbar2GTK3 as NavigationToolbar -from matplotlib.backends.backend_gtk3agg import \ - FigureCanvasGTK3Agg as FigureCanvas +from matplotlib.backends.backend_gtk3 import NavigationToolbar2GTK3 as NavigationToolbar +from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas from matplotlib.figure import Figure win = Gtk.Window() diff --git a/galleries/examples/user_interfaces/embedding_in_gtk3_sgskip.py b/galleries/examples/user_interfaces/embedding_in_gtk3_sgskip.py index 51ceebb501e3..3ddff529b298 100644 --- a/galleries/examples/user_interfaces/embedding_in_gtk3_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_gtk3_sgskip.py @@ -14,8 +14,7 @@ import numpy as np -from matplotlib.backends.backend_gtk3agg import \ - FigureCanvasGTK3Agg as FigureCanvas +from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas from matplotlib.figure import Figure win = Gtk.Window() diff --git a/galleries/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py b/galleries/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py index e42e59459198..4dec7a219d4e 100644 --- a/galleries/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py @@ -13,10 +13,8 @@ import numpy as np -from matplotlib.backends.backend_gtk4 import \ - NavigationToolbar2GTK4 as NavigationToolbar -from matplotlib.backends.backend_gtk4agg import \ - FigureCanvasGTK4Agg as FigureCanvas +from matplotlib.backends.backend_gtk4 import NavigationToolbar2GTK4 as NavigationToolbar +from matplotlib.backends.backend_gtk4agg import FigureCanvasGTK4Agg as FigureCanvas from matplotlib.figure import Figure @@ -44,10 +42,9 @@ def on_activate(app): toolbar = NavigationToolbar(canvas) vbox.append(toolbar) - win.show() + win.present() -app = Gtk.Application( - application_id='org.matplotlib.examples.EmbeddingInGTK4PanZoom') +app = Gtk.Application(application_id='org.matplotlib.examples.EmbeddingInGTK4PanZoom') app.connect('activate', on_activate) app.run(None) diff --git a/galleries/examples/user_interfaces/embedding_in_gtk4_sgskip.py b/galleries/examples/user_interfaces/embedding_in_gtk4_sgskip.py index 197cd7971088..bc90700e48d3 100644 --- a/galleries/examples/user_interfaces/embedding_in_gtk4_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_gtk4_sgskip.py @@ -14,8 +14,7 @@ import numpy as np -from matplotlib.backends.backend_gtk4agg import \ - FigureCanvasGTK4Agg as FigureCanvas +from matplotlib.backends.backend_gtk4agg import FigureCanvasGTK4Agg as FigureCanvas from matplotlib.figure import Figure @@ -39,7 +38,7 @@ def on_activate(app): canvas.set_size_request(800, 600) sw.set_child(canvas) - win.show() + win.present() app = Gtk.Application(application_id='org.matplotlib.examples.EmbeddingInGTK4') diff --git a/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py b/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py index cea1a89c29df..35a22efd67ec 100644 --- a/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_qt_sgskip.py @@ -15,8 +15,7 @@ import numpy as np from matplotlib.backends.backend_qtagg import FigureCanvas -from matplotlib.backends.backend_qtagg import \ - NavigationToolbar2QT as NavigationToolbar +from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar from matplotlib.backends.qt_compat import QtWidgets from matplotlib.figure import Figure diff --git a/galleries/examples/user_interfaces/embedding_in_tk_sgskip.py b/galleries/examples/user_interfaces/embedding_in_tk_sgskip.py index 7474f40b4bac..2fa132a80227 100644 --- a/galleries/examples/user_interfaces/embedding_in_tk_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_in_tk_sgskip.py @@ -11,8 +11,7 @@ # Implement the default Matplotlib key bindings. from matplotlib.backend_bases import key_press_handler -from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, - NavigationToolbar2Tk) +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk from matplotlib.figure import Figure root = tkinter.Tk() diff --git a/galleries/examples/user_interfaces/embedding_webagg_sgskip.py b/galleries/examples/user_interfaces/embedding_webagg_sgskip.py index cdeb6419a18e..40d8a718facc 100644 --- a/galleries/examples/user_interfaces/embedding_webagg_sgskip.py +++ b/galleries/examples/user_interfaces/embedding_webagg_sgskip.py @@ -31,8 +31,8 @@ import numpy as np import matplotlib as mpl -from matplotlib.backends.backend_webagg import ( - FigureManagerWebAgg, new_figure_manager_given_figure) +from matplotlib.backends.backend_webagg import (FigureManagerWebAgg, + new_figure_manager_given_figure) from matplotlib.figure import Figure diff --git a/galleries/examples/user_interfaces/mpl_with_glade3_sgskip.py b/galleries/examples/user_interfaces/mpl_with_glade3_sgskip.py index f39dbf4ca28e..8321405aa011 100644 --- a/galleries/examples/user_interfaces/mpl_with_glade3_sgskip.py +++ b/galleries/examples/user_interfaces/mpl_with_glade3_sgskip.py @@ -13,8 +13,7 @@ import numpy as np -from matplotlib.backends.backend_gtk3agg import \ - FigureCanvasGTK3Agg as FigureCanvas +from matplotlib.backends.backend_gtk3agg import FigureCanvasGTK3Agg as FigureCanvas from matplotlib.figure import Figure diff --git a/galleries/examples/user_interfaces/web_application_server_sgskip.py b/galleries/examples/user_interfaces/web_application_server_sgskip.py index 60c321e02eb9..f125916db54b 100644 --- a/galleries/examples/user_interfaces/web_application_server_sgskip.py +++ b/galleries/examples/user_interfaces/web_application_server_sgskip.py @@ -5,7 +5,7 @@ When using Matplotlib in a web server it is strongly recommended to not use pyplot (pyplot maintains references to the opened figures to make -`~.matplotlib.pyplot.show` work, but this will cause memory leaks unless the +`~.pyplot.show` work, but this will cause memory leaks unless the figures are properly closed). Since Matplotlib 3.1, one can directly create figures using the `.Figure` @@ -45,21 +45,14 @@ def hello(): # %% # # Since the above code is a Flask application, it should be run using the -# `flask command-line tool `_ -# Assuming that the working directory contains this script: -# -# Unix-like systems +# `flask command-line tool `_: +# run # # .. code-block:: console # -# FLASK_APP=web_application_server_sgskip flask run -# -# Windows -# -# .. code-block:: console +# flask --app web_application_server_sgskip run # -# set FLASK_APP=web_application_server_sgskip -# flask run +# from the directory containing this script. # # # Clickable images for HTML diff --git a/galleries/examples/widgets/multicursor.py b/galleries/examples/widgets/multicursor.py index bc0d58b6c749..9123c31f63f4 100644 --- a/galleries/examples/widgets/multicursor.py +++ b/galleries/examples/widgets/multicursor.py @@ -26,7 +26,7 @@ fig, ax3 = plt.subplots() ax3.plot(t, s3) -multi = MultiCursor(None, (ax1, ax2, ax3), color='r', lw=1) +multi = MultiCursor((ax1, ax2, ax3), color='r', lw=1) plt.show() # %% diff --git a/galleries/tutorials/coding_shortcuts.py b/galleries/tutorials/coding_shortcuts.py new file mode 100644 index 000000000000..46868482598f --- /dev/null +++ b/galleries/tutorials/coding_shortcuts.py @@ -0,0 +1,172 @@ +""" +================ +Coding shortcuts +================ + +Matplotlib's primary and universal API is the :ref:`Axes interface `. +While it is clearly structured and powerful, it can sometimes feel overly verbose and +thus cumbersome to write. This page collects patterns for condensing the code +of the Axes-based API and achieving the same results with less typing for many simpler +plots. + +.. note:: + + The :ref:`pyplot interface ` is an alternative more compact + interface, and was historically modeled to be similar to MATLAB. It remains a + valid approach for those who want to use it. However, it has the disadvantage that + it achieves its brevity through implicit assumptions that you have to understand. + + Since it follows a different paradigm, switching between the Axes interface and + the pyplot interface requires a shift of the mental model, and some code rewrite, + if the code develops to a point at which pyplot no longer provides enough + flexibility. + +This tutorial goes the other way round, starting from the standard verbose Axes +interface and using its capabilities for shortcuts when you don't need all the +generality. + +Let's assume we want to make a plot of the number of daylight hours per day over the +year in London. + +The standard approach with the Axes interface looks like this. +""" + +import matplotlib.pyplot as plt +import numpy as np + +day = np.arange(365) +hours = 4.276 * np.sin(2 * np.pi * (day - 80)/365) + 12.203 + +fig, ax = plt.subplots() +ax.plot(day, hours, color="orange") +ax.set_xlabel("day") +ax.set_ylabel("daylight hours") +ax.set_title("London") +plt.show() + +# %% +# Note that we've included ``plt.show()`` here. This is needed to show the plot window +# when running from a command line or in a Python script. If you run a Jupyter notebook, +# this command is automatically executed at the end of each cell. +# +# For the rest of the tutorial, we'll assume that we are in a notebook and leave this +# out for brevity. Depending on your context you may still need it. +# +# If you instead want to save to a file, use ``fig.savefig("daylight.png")``. +# +# +# Collect Axes properties into a single ``set()`` call +# ==================================================== +# +# The properties of Matplotlib Artists can be modified through their respective +# ``set_*()`` methods. Artists additionally have a generic ``set()`` method, that takes +# keyword arguments and is equivalent to calling all the respective ``set_*()`` methods. +# :: +# +# ax.set_xlabel("day") +# ax.set_ylabel("daylight hours") +# +# can also be written as :: +# +# ax.set(xlabel="day", ylabel="daylight hours") +# +# This is the most simple and effective reduction you can do. With that we can shorten +# the above plot to + +fig, ax = plt.subplots() +ax.plot(day, hours, color="orange") +ax.set(xlabel="day", ylabel="daylight hours", title="London") + +# %% +# +# This works as long as you only need to pass one parameter to the ``set_*()`` function. +# The individual functions are still necessary if you want more control, e.g. +# ``set_title("London", fontsize=16)``. +# +# +# Not storing a reference to the figure +# ===================================== +# Another nuisance of ``fig, ax = plt.subplots()`` is that you always create a ``fig`` +# variable, even if you don't use it. A slightly shorter version would be using the +# standard variable for unused value in Python (``_``): ``_, ax = plt.subplots()``. +# However, that's only marginally better. +# +# You can work around this by separating figure and Axes creation and chaining them :: +# +# ax = plt.figure().add_subplot() +# +# This is a bit cleaner logically and has the slight advantage that you could set +# figure properties inline as well; e.g. ``plt.figure(facecolor="lightgoldenrod")``. +# But it has the disadvantage that it's longer than ``fig, ax = plt.subplots()``. +# +# You can still obtain the figure from the Axes if needed, e.g. :: +# +# ax.figure.savefig("daylight_hours.png") +# +# The example code now looks like this: + +ax = plt.figure().add_subplot() +ax.plot(day, hours, color="orange") +ax.set(xlabel="day", ylabel="daylight hours", title="London") + +# %% +# Define Axes properties during axes creation +# =========================================== +# The ``set_*`` methods as well as ``set`` modify existing objects. You can +# alternatively define them right at creation. Since you typically don't instantiate +# classes yourself in Matplotlib, but rather call some factory function that creates +# the object and wires it up correctly with the plot, this may seem less obvious. But +# in fact you just pass the desired properties to the factory functions. You are likely +# doing this already in some places without realizing. Consider the function to create +# a line :: +# +# ax.plot(x, y, color="orange") +# +# This is equivalent to :: +# +# line, = ax.plot(x, y) +# line.set_color("orange") +# +# The same can be done with functions that create Axes. + +ax = plt.figure().add_subplot(xlabel="day", ylabel="daylight hours", title="London") +ax.plot(day, hours, color="orange") + +# %% +# .. important:: +# The Axes properties are only accepted as keyword arguments by +# `.Figure.add_subplot`, which creates a single Axes. +# +# For `.Figure.subplots` and `.pyplot.subplots`, you'd have to pass the properties +# as a dict via the keyword argument ``subplot_kw``. The limitation here is that +# such parameters are given to all Axes. For example, if you need two subplots +# (``fig, (ax1, ax2) = plt.subplots(1, 2)``) with different labels, you have to +# set them individually. +# +# Defining Axes properties during creation is best used for single subplots or when +# all subplots share the same properties. +# +# +# Using implicit figure creation +# ============================== +# You can go even further by tapping into the pyplot logic and use `.pyplot.axes` to +# create the axes: + +ax = plt.axes(xlabel="day", ylabel="daylight hours", title="London") +ax.plot(day, hours, color="orange") + +# %% +# .. warning:: +# When using this, you have to be aware of the implicit figure semantics of pyplot. +# ``plt.axes()`` will only create a new figure if no figure exists. Otherwise, it +# will add the Axes to the current existing figure, which is likely not what you +# want. +# +# Not storing a reference to the Axes +# =================================== +# If you only need to visualize one dataset, you can append the plot command +# directly to the Axes creation. This may be useful e.g. in notebooks, +# where you want to create a plot with some configuration, but as little distracting +# code as possible: + +plt.axes(xlabel="day", ylabel="daylight hours").plot(day, hours, color="orange") diff --git a/galleries/tutorials/images.py b/galleries/tutorials/images.py index a7c474dab40b..6c4e68c32416 100644 --- a/galleries/tutorials/images.py +++ b/galleries/tutorials/images.py @@ -7,40 +7,6 @@ Image tutorial ============== -A short tutorial on plotting images with Matplotlib. - -.. _imaging_startup: - -Startup commands -=================== - -First, let's start IPython. It is a most excellent enhancement to the -standard Python prompt, and it ties in especially well with -Matplotlib. Start IPython either directly at a shell, or with the Jupyter -Notebook (where IPython as a running kernel). - -With IPython started, we now need to connect to a GUI event loop. This -tells IPython where (and how) to display plots. To connect to a GUI -loop, execute the **%matplotlib** magic at your IPython prompt. There's more -detail on exactly what this does at `IPython's documentation on GUI -event loops -`_. - -If you're using Jupyter Notebook, the same commands are available, but -people commonly use a specific argument to the %matplotlib magic: - -.. sourcecode:: ipython - - In [1]: %matplotlib inline - -This turns on inline plotting, where plot graphics will appear in your notebook. This -has important implications for interactivity. For inline plotting, commands in -cells below the cell that outputs a plot will not affect the plot. For example, -changing the colormap is not possible from cells below the cell that creates a plot. -However, for other backends, such as Qt, that open a separate window, -cells below those that create the plot will change the plot - it is a -live object in memory. - This tutorial will use Matplotlib's implicit plotting interface, pyplot. This interface maintains global state, and is very useful for quickly and easily experimenting with various plot settings. The alternative is the explicit, @@ -147,15 +113,6 @@ # %% # -# .. note:: -# -# However, remember that in the Jupyter Notebook with the inline backend, -# you can't make changes to plots that have already been rendered. If you -# create imgplot here in one cell, you cannot call set_cmap() on it in a later -# cell and expect the earlier plot to change. Make sure that you enter these -# commands together in one cell. plt commands will not change plots from earlier -# cells. -# # There are many other colormap schemes available. See the :ref:`list and images # of the colormaps`. # @@ -201,9 +158,7 @@ # %% # This can also be done by calling the # :meth:`~matplotlib.cm.ScalarMappable.set_clim` method of the returned image -# plot object, but make sure that you do so in the same cell as your plot -# command when working with the Jupyter Notebook - it will not change -# plots from earlier cells. +# plot object. imgplot = plt.imshow(lum_img) imgplot.set_clim(0, 175) diff --git a/galleries/tutorials/index.rst b/galleries/tutorials/index.rst index 48187a862a2e..76c0037dca11 100644 --- a/galleries/tutorials/index.rst +++ b/galleries/tutorials/index.rst @@ -32,6 +32,23 @@ a :ref:`FAQ ` in our :ref:`user guide `. +.. raw:: html + +
+ +.. only:: html + + .. image:: /tutorials/images/thumb/sphx_glr_coding_shortcuts_thumb.png + :alt: Coding shortcuts + + :ref:`sphx_glr_tutorials_coding_shortcuts.py` + +.. raw:: html + +
Coding shortcuts
+
+ + .. raw:: html
@@ -92,6 +109,7 @@ a :ref:`FAQ ` in our :ref:`user guide `. :hidden: /tutorials/pyplot + /tutorials/coding_shortcuts /tutorials/images /tutorials/lifecycle /tutorials/artists diff --git a/galleries/users_explain/artists/color_cycle.py b/galleries/users_explain/artists/color_cycle.py index aa978e62b269..58170fad5b58 100644 --- a/galleries/users_explain/artists/color_cycle.py +++ b/galleries/users_explain/artists/color_cycle.py @@ -52,7 +52,7 @@ # %% # Now we'll generate a figure with two Axes, one on top of the other. On the -# first axis, we'll plot with the default cycler. On the second axis, we'll +# first axes, we'll plot with the default cycler. On the second axes, we'll # set the ``prop_cycle`` using :func:`matplotlib.axes.Axes.set_prop_cycle`, # which will only set the ``prop_cycle`` for this :mod:`matplotlib.axes.Axes` # instance. We'll use a second ``cycler`` that combines a color cycler and a diff --git a/galleries/users_explain/artists/imshow_extent.py b/galleries/users_explain/artists/imshow_extent.py index d16a15f1e9f9..a6daa3a541c1 100644 --- a/galleries/users_explain/artists/imshow_extent.py +++ b/galleries/users_explain/artists/imshow_extent.py @@ -3,8 +3,8 @@ .. _imshow_extent: -*origin* and *extent* in `~.Axes.imshow` -======================================== +Positioning and orientation of `~.Axes.imshow` images +===================================================== :meth:`~.Axes.imshow` allows you to render an image (either a 2D array which will be color-mapped (based on *norm* and *cmap*) or a 3D RGB(A) array which diff --git a/galleries/users_explain/artists/transforms_tutorial.py b/galleries/users_explain/artists/transforms_tutorial.py index 3920fe886c7f..1a25f1f87c88 100644 --- a/galleries/users_explain/artists/transforms_tutorial.py +++ b/galleries/users_explain/artists/transforms_tutorial.py @@ -64,10 +64,9 @@ | |is top right of the output in | | | |"display units". | | | | | | -| |The exact interpretation of the | | -| |units depends on the back end. For | | -| |example it is pixels for Agg and | | -| |points for svg/pdf. | | +| |"Display units" depends on the | | +| |backend. For example, Agg uses | | +| |pixels, and SVG/PDF use points. | | +----------------+-----------------------------------+-----------------------------+ The `~matplotlib.transforms.Transform` objects are naive to the source and diff --git a/galleries/users_explain/axes/autoscale.py b/galleries/users_explain/axes/autoscale.py index df1fbbc8aea8..337960302c38 100644 --- a/galleries/users_explain/axes/autoscale.py +++ b/galleries/users_explain/axes/autoscale.py @@ -18,7 +18,6 @@ import matplotlib.pyplot as plt import numpy as np -import matplotlib as mpl x = np.linspace(-2 * np.pi, 2 * np.pi, 100) y = np.sinc(x) @@ -159,22 +158,3 @@ ax.autoscale(enable=None, axis="x", tight=True) print(ax.margins()) - -# %% -# Working with collections -# ------------------------ -# -# Autoscale works out of the box for all lines, patches, and images added to -# the Axes. One of the artists that it won't work with is a `.Collection`. -# After adding a collection to the Axes, one has to manually trigger the -# `~matplotlib.axes.Axes.autoscale_view()` to recalculate -# axes limits. - -fig, ax = plt.subplots() -collection = mpl.collections.StarPolygonCollection( - 5, rotation=0, sizes=(250,), # five point star, zero angle, size 250px - offsets=np.column_stack([x, y]), # Set the positions - offset_transform=ax.transData, # Propagate transformations of the Axes -) -ax.add_collection(collection) -ax.autoscale_view() diff --git a/galleries/users_explain/axes/constrainedlayout_guide.py b/galleries/users_explain/axes/constrainedlayout_guide.py index 4eeaed980843..5c2749804740 100644 --- a/galleries/users_explain/axes/constrainedlayout_guide.py +++ b/galleries/users_explain/axes/constrainedlayout_guide.py @@ -164,7 +164,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # Legends # ======= # -# Legends can be placed outside of their parent axis. +# Legends can be placed outside of their parent axes. # *Constrained layout* is designed to handle this for :meth:`.Axes.legend`. # However, *constrained layout* does *not* handle legends being created via # :meth:`.Figure.legend` (yet). diff --git a/galleries/users_explain/colors/colormapnorms.py b/galleries/users_explain/colors/colormapnorms.py index af50cef357b3..66bff534b3b0 100644 --- a/galleries/users_explain/colors/colormapnorms.py +++ b/galleries/users_explain/colors/colormapnorms.py @@ -281,6 +281,32 @@ cb.set_ticks([-500, 0, 1000, 2000, 3000, 4000]) plt.show() +# %% +# Using a linear scale on the colormap +# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +# +# By default, colorbars adopt the same axis scaling as their associated norm. +# For example, for a `.TwoSlopeNorm`, colormap segments are distributed +# linearly and the colorbar ticks positions are spaced non-linearly (as above, +# and the left-hand colorbar below). To make the tick spacing linear instead, +# you can change the scale by calling ``cb.ax.set_yscale('linear')``, as shown +# in the right-hand colorbar below. The ticks will then be evenly spaced, the +# colormap will appear compressed in the smaller of the two slope regions. + +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(10, 4)) +divnorm = colors.TwoSlopeNorm(vmin=-500., vcenter=0, vmax=4000) + +for ax, title in zip([ax1, ax2], + ['Default: Scaled colorbar', 'Linear colorbar spacing']): + pcm = ax.pcolormesh(longitude, latitude, topo, rasterized=True, norm=divnorm, + cmap=terrain_map, shading='auto') + ax.set_aspect(1 / np.cos(np.deg2rad(49))) + ax.set_title(title) + cb = fig.colorbar(pcm, ax=ax, shrink=0.6) + cb.set_ticks(np.arange(-500, 4001, 500)) + +# Set linear scale for the right colorbar +cb.ax.set_yscale('linear') # %% # FuncNorm: Arbitrary function normalization @@ -290,6 +316,7 @@ # `~.colors.FuncNorm` to define your own. Note that this example is the same # as `~.colors.PowerNorm` with a power of 0.5: + def _forward(x): return np.sqrt(x) diff --git a/galleries/users_explain/colors/colormaps.py b/galleries/users_explain/colors/colormaps.py index 026ffc9922e2..263c3c72e8ea 100644 --- a/galleries/users_explain/colors/colormaps.py +++ b/galleries/users_explain/colors/colormaps.py @@ -161,11 +161,26 @@ def plot_color_gradients(category, cmap_list): # an excellent example of this). plot_color_gradients('Sequential (2)', - ['binary', 'gist_yarg', 'gist_gray', 'gray', 'bone', - 'pink', 'spring', 'summer', 'autumn', 'winter', 'cool', - 'Wistia', 'hot', 'afmhot', 'gist_heat', 'copper']) + ['gray', 'bone', 'pink', 'spring', 'summer', 'autumn', + 'winter', 'cool', 'Wistia', 'hot', 'afmhot', 'gist_heat', + 'copper']) # %% +# .. admonition:: Discouraged +# +# For backward compatibility we additionally support the following colormap +# names, which are identical to other builtin colormaps. Their use is +# discouraged. Use the suggested replacement instead. +# +# ========= ================================= +# Colormap Use identical replacement instead +# ========= ================================= +# gist_gray gray +# gist_yarg gray_r +# binary gray_r +# ========= ================================= +# +# # Diverging # --------- # @@ -215,9 +230,9 @@ def plot_color_gradients(category, cmap_list): # These would not be good options for use as perceptual colormaps. plot_color_gradients('Qualitative', - ['Pastel1', 'Pastel2', 'Paired', 'Accent', 'Dark2', - 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', - 'tab20c']) + ['Pastel1', 'Pastel2', 'Paired', 'Accent', 'okabe_ito', + 'Dark2', 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', + 'tab20b', 'tab20c']) # %% # Miscellaneous diff --git a/galleries/users_explain/configuration.py b/galleries/users_explain/configuration.py new file mode 100644 index 000000000000..d5f7f38e98ff --- /dev/null +++ b/galleries/users_explain/configuration.py @@ -0,0 +1,14 @@ +""" +.. _rcparams_reference: + +Matplotlib configuration - rcParams +=================================== +Matplotlib's configuration parameters (rcParams) control the behavior and +appearance of plots. These parameters are stored in a dict-like variable +called :data:`matplotlib.rcParams` (also accessible via ``plt.rcParams``). + +The following is a comprehensive reference of all available rcParams and their +default values. + +.. include:: /users/_rcparams_generated.rst +""" diff --git a/galleries/users_explain/customizing.py b/galleries/users_explain/customizing.py index 05b75ba7d0a4..948727c6a165 100644 --- a/galleries/users_explain/customizing.py +++ b/galleries/users_explain/customizing.py @@ -8,9 +8,12 @@ Customizing Matplotlib with style sheets and rcParams ===================================================== -Tips for customizing the properties and default styles of Matplotlib. +Many aspects of Matplotlib's behavior and default styles can be customized +through the use of rc (runtime configuration) settings. The current values +are stored in `~matplotlib.rcParams`. -There are three ways to customize Matplotlib: +There are three ways to customize Matplotlib, all of which effectively change +`~matplotlib.rcParams`: 1. :ref:`Setting rcParams at runtime`. 2. :ref:`Using style sheets`. diff --git a/galleries/users_explain/figure/api_interfaces.rst b/galleries/users_explain/figure/api_interfaces.rst index 981359dbee0b..c3ac06aa27ab 100644 --- a/galleries/users_explain/figure/api_interfaces.rst +++ b/galleries/users_explain/figure/api_interfaces.rst @@ -148,7 +148,7 @@ interfaces and how to translate from one to the other. - Axes: ``label = ax.get_xlabel()`` - pyplot: ``label = plt.xlabel()`` -- Functions that set properties like the property in pyplot and are prefixed with +- Functions that set properties are named like the property in pyplot and are prefixed with ``set_`` on the Axes. Example: - Axes: ``ax.set_xlabel("time")`` @@ -174,7 +174,7 @@ referenced by ``plt.gca()``? One simple way is to call ``subplot`` again with the same arguments. However, that quickly becomes inelegant. You can also inspect the Figure object and get its list of Axes objects, however, that can be misleading (colorbars are Axes too!). The best solution is probably to save a -handle to every Axes you create, but if you do that, why not simply create the +handle to every Axes you create, but if you do that, why not simply create all the Axes objects at the start? The first approach is to call ``plt.subplot`` again: diff --git a/galleries/users_explain/figure/backends.rst b/galleries/users_explain/figure/backends.rst index 85ed8dece23d..69f6d61dc563 100644 --- a/galleries/users_explain/figure/backends.rst +++ b/galleries/users_explain/figure/backends.rst @@ -253,6 +253,35 @@ backend, use ``module://name.of.the.backend`` as the backend name, e.g. Information for backend implementers is available at :ref:`writing_backend_interface`. +Backend API versions +-------------------- +Matplotlib aims to maintain backward compatibility on backends. Nevertheless, we +want to be able to evolve the backend API to support new features. Defining backend +API versions will help to communicate which API is supported by a given version of +Matplotlib. + +The following backend API versions exist + +.. list-table:: + :header-rows: 1 + + * - API version + - Supported since + - Description + * - 1.0 + - Matplotlib 3.10 + - This is the starting point for systematic definition of backend versions. + Most of the API will work far back, but there is no benefit in retroactively + uncovering all prior the changes. + * - 1.1 + - Matplotlib 3.11 + - `.RendererBase.draw_path_collection` gained a new optional parameter + *hatchcolor*. The presence of the parameter is inferred by introspection, so + that matplotlib 3.11+ will still work with backends implementing API version + 1.0. + +There is currently no plan to remove support for older API versions. + .. _figures-not-showing: Debugging the figure windows not showing diff --git a/galleries/users_explain/figure/event_handling.rst b/galleries/users_explain/figure/event_handling.rst index caf987e3d6da..8b4928eafb52 100644 --- a/galleries/users_explain/figure/event_handling.rst +++ b/galleries/users_explain/figure/event_handling.rst @@ -605,7 +605,7 @@ Picking exercise Create a data set of 100 arrays of 1000 Gaussian random numbers and compute the sample mean and standard deviation of each of them (hint: -NumPy arrays have a mean and std method) and make a xy marker plot of +NumPy arrays have a mean and std method) and make an xy marker plot of the 100 means vs. the 100 standard deviations. Connect the line created by the plot command to the pick event, and plot the original time series of the data that generated the clicked on points. If more diff --git a/galleries/users_explain/text/annotations.py b/galleries/users_explain/text/annotations.py index 5cfb16c12715..b0eff8d19f7d 100644 --- a/galleries/users_explain/text/annotations.py +++ b/galleries/users_explain/text/annotations.py @@ -12,6 +12,7 @@ .. redirect-from:: /gallery/userdemo/connect_simple01 .. redirect-from:: /gallery/userdemo/connectionstyle_demo .. redirect-from:: /tutorials/text/annotations +.. redirect-from:: /gallery/text_labels_and_annotations/annotate_transform .. _annotations: diff --git a/galleries/users_explain/text/fonts.py b/galleries/users_explain/text/fonts.py index 067ed2f3932a..40cc9eaa93eb 100644 --- a/galleries/users_explain/text/fonts.py +++ b/galleries/users_explain/text/fonts.py @@ -27,28 +27,24 @@ Matplotlib supports three font specifications (in addition to pdf 'core fonts', which are explained later in the guide): -.. table:: Type of Fonts - - +--------------------------+----------------------------+----------------------------+ - | Type 1 (PDF with usetex) | Type 3 (PDF/PS) | TrueType (PDF) | - +==========================+============================+============================+ - | One of the oldest types, | Similar to Type 1 in | Newer than previous types, | - | introduced by Adobe | terms of introduction | used commonly today, | - | | | introduced by Apple | - +--------------------------+----------------------------+----------------------------+ - | Restricted subset of | Full PostScript language, | Includes a virtual machine | - | PostScript, charstrings | allows embedding arbitrary | that can execute code! | - | are in bytecode | code (in theory, even | | - | | render fractals when | | - | | rasterizing!) | | - +--------------------------+----------------------------+----------------------------+ - | Supports font | Does not support font | Supports font hinting | - | hinting | hinting | (virtual machine processes | - | | | the "hints") | - +--------------------------+----------------------------+----------------------------+ - | Subsetted by code in | Subsetted via external module | - | `matplotlib._type1font` | `fontTools `__ | - +--------------------------+----------------------------+----------------------------+ +.. table:: Types of Fonts + + +--------------------------+----------------------------+-------------------------------+ + | Type 1 (PDF with usetex) | Type 3 (PDF/PS) | TrueType (PDF) / Type 42 (PS) | + +==========================+============================+===============================+ + | Old font types introduced by Adobe. | Newer font type introduced by | + | | Apple; commonly used today. | + +--------------------------+----------------------------+-------------------------------+ + | Restricted subset of | Full PostScript language, | Includes a virtual machine | + | PostScript, charstrings | allows embedding arbitrary | that can execute code. | + | are in bytecode. | code. | | + +--------------------------+----------------------------+-------------------------------+ + | Supports font hinting. | Does not support font | Supports font hinting, | + | | hinting. | through the virtual machine. | + +--------------------------+----------------------------+-------------------------------+ + | Subsetted by code in | Subsetted via external module | + | `matplotlib._type1font`. | `fontTools `__. | + +--------------------------+----------------------------+-------------------------------+ .. note:: @@ -59,23 +55,9 @@ __ https://helpx.adobe.com/fonts/kb/postscript-type-1-fonts-end-of-support.html -Other font specifications which Matplotlib supports: - -- Type 42 fonts (PS): - - - PostScript wrapper around TrueType fonts - - 42 is the `Answer to Life, the Universe, and Everything! - `_ - - Matplotlib uses the external library - `fontTools `__ to subset these types of - fonts - -- OpenType fonts: - - - OpenType is a new standard for digital type fonts, developed jointly by - Adobe and Microsoft - - Generally contain a much larger character set! - - Limited support with Matplotlib +Matplotlib also provides limited support for OpenType fonts, a newer standard +developed jointly by Adobe and Microsoft; such fonts generally contain a much +larger character set. Font subsetting ^^^^^^^^^^^^^^^ @@ -201,4 +183,4 @@ A majority of this work was done by Aitik Gupta supported by Google Summer of Code 2021. -""" +""" # noqa: E501 diff --git a/galleries/users_explain/text/mathtext.py b/galleries/users_explain/text/mathtext.py index 7ff317804f98..4a4f80c12695 100644 --- a/galleries/users_explain/text/mathtext.py +++ b/galleries/users_explain/text/mathtext.py @@ -370,4 +370,4 @@ # If a particular symbol does not have a name (as is true of many of the more # obscure symbols in the STIX fonts), Unicode characters can also be used:: # -# r'$\u23ce$' +# '$\u23ce$' diff --git a/galleries/users_explain/text/text_props.py b/galleries/users_explain/text/text_props.py index fb67421fd880..50eb53dccbdd 100644 --- a/galleries/users_explain/text/text_props.py +++ b/galleries/users_explain/text/text_props.py @@ -34,7 +34,7 @@ picker [None|float|bool|callable] position (x, y) rotation [ angle in degrees | ``'vertical'`` | ``'horizontal'`` ] -size or fontsize [ size in points | relative size, e.g., ``'smaller'``, ``'x-large'`` ] + size or fontsize [ size in points | relative size, e.g., ``'small'``, ``'x-large'`` ] style or fontstyle [ ``'normal'`` | ``'italic'`` | ``'oblique'`` ] text string or anything printable with '%s' conversion transform `~matplotlib.transforms.Transform` subclass @@ -48,6 +48,9 @@ ========================== ====================================================================================================================== +Text alignment +============== + You can lay out text with the alignment arguments ``horizontalalignment``, ``verticalalignment``, and ``multialignment``. ``horizontalalignment`` controls whether the x @@ -69,8 +72,8 @@ import matplotlib.patches as patches # build a rectangle in axes coords -left, width = .25, .5 -bottom, height = .25, .5 +left, width = 0.25, 0.5 +bottom, height = 0.25, 0.5 right = left + width top = bottom + height @@ -144,6 +147,25 @@ plt.show() # %% +# Relative font sizes +# =================== +# +# Font sizes can be specified in points, or as a string that indicates the size +# relative to the default font size. The following are valid values for relative font sizes: +# +# ==================== ================================ +# Relative font size Scaling of default font size +# ==================== ================================ +# xx-small 0.579 +# x-small 0.694 +# small 0.833 +# medium 1.000 +# large 1.200 +# x-large 1.440 +# xx-large 1.728 +# ==================== ================================ +# +# # ============== # Default Font # ============== diff --git a/galleries/users_explain/toolkits/axisartist.rst b/galleries/users_explain/toolkits/axisartist.rst index 7ff0897f23d8..a5a1b6cbcd78 100644 --- a/galleries/users_explain/toolkits/axisartist.rst +++ b/galleries/users_explain/toolkits/axisartist.rst @@ -131,7 +131,7 @@ few things that mpl_toolkits.axisartist.Axes is different from original Axes from Matplotlib. * Axis elements (axis line(spine), ticks, ticklabel and axis labels) - are drawn by a AxisArtist instance. Unlike Axis, left, right, top + are drawn by an AxisArtist instance. Unlike Axis, left, right, top and bottom axis are drawn by separate artists. And each of them may have different tick location and different tick labels. @@ -569,7 +569,7 @@ See the first example of this page. Current limitations and TODO's ============================== -The code need more refinement. Here is a incomplete list of issues and TODO's +The code need more refinement. Here is an incomplete list of issues and TODO's * No easy way to support a user customized tick location (for curvilinear grid). A new Locator class needs to be created. diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index e98e8ea07502..167d6924a72e 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -158,7 +158,7 @@ # cbook must import matplotlib only within function # definitions, so it is safe to import from it here. -from . import _api, _version, cbook, _docstring, rcsetup +from . import _api, _version, cbook, rcsetup from matplotlib._api import MatplotlibDeprecationWarning from matplotlib.colors import _color_sequences as color_sequences from matplotlib.rcsetup import cycler # noqa: F401 @@ -292,8 +292,8 @@ def set_loglevel(level): - set the root logger handler's level, creating the handler if it does not exist yet - Typically, one should call ``set_loglevel("info")`` or - ``set_loglevel("debug")`` to get additional debugging information. + Typically, one should call ``set_loglevel("INFO")`` or + ``set_loglevel("DEBUG")`` to get additional debugging information. Users or applications that are installing their own logging handlers may want to directly manipulate ``logging.getLogger('matplotlib')`` rather @@ -301,8 +301,12 @@ def set_loglevel(level): Parameters ---------- - level : {"notset", "debug", "info", "warning", "error", "critical"} - The log level of the handler. + level : {"NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} + The log level as defined in `Python logging levels + `__. + + For backwards compatibility, the levels are case-insensitive, but + the capitalized version is preferred in analogy to `logging.Logger.setLevel`. Notes ----- @@ -528,19 +532,30 @@ def _get_config_or_cache_dir(xdg_base_getter): elif sys.platform.startswith(('linux', 'freebsd')): # Only call _xdg_base_getter here so that MPLCONFIGDIR is tried first, # as _xdg_base_getter can throw. - configdir = Path(xdg_base_getter(), "matplotlib") + try: + configdir = Path(xdg_base_getter(), "matplotlib") + except RuntimeError: # raised if Path.home() is not available + pass else: - configdir = Path.home() / ".matplotlib" - # Resolve the path to handle potential issues with inaccessible symlinks. - configdir = configdir.resolve() - try: - configdir.mkdir(parents=True, exist_ok=True) - except OSError as exc: - _log.warning("mkdir -p failed for path %s: %s", configdir, exc) + try: + configdir = Path.home() / ".matplotlib" + except RuntimeError: # raised if Path.home() is not available + pass + + if configdir: + # Resolve the path to handle potential issues with inaccessible symlinks. + configdir = configdir.resolve() + try: + configdir.mkdir(parents=True, exist_ok=True) + except OSError as exc: + _log.warning("mkdir -p failed for path %s: %s", configdir, exc) + else: + if os.access(str(configdir), os.W_OK) and configdir.is_dir(): + return str(configdir) + _log.warning("%s is not a writable directory", configdir) + issue_msg = "the default path ({configdir})" else: - if os.access(str(configdir), os.W_OK) and configdir.is_dir(): - return str(configdir) - _log.warning("%s is not a writable directory", configdir) + issue_msg = "resolving the home directory" # If the config or cache directory cannot be created or is not a writable # directory, create a temporary one. try: @@ -548,17 +563,17 @@ def _get_config_or_cache_dir(xdg_base_getter): except OSError as exc: raise OSError( f"Matplotlib requires access to a writable cache directory, but there " - f"was an issue with the default path ({configdir}), and a temporary " + f"was an issue with {issue_msg}, and a temporary " f"directory could not be created; set the MPLCONFIGDIR environment " f"variable to a writable directory") from exc os.environ["MPLCONFIGDIR"] = tmpdir atexit.register(shutil.rmtree, tmpdir) _log.warning( "Matplotlib created a temporary cache directory at %s because there was " - "an issue with the default path (%s); it is highly recommended to set the " + "an issue with %s; it is highly recommended to set the " "MPLCONFIGDIR environment variable to a writable directory, in particular to " "speed up the import of Matplotlib and to better support multiprocessing.", - tmpdir, configdir) + tmpdir, issue_msg) return tmpdir @@ -643,23 +658,13 @@ def gen_candidates(): "install is broken") -@_docstring.Substitution( - "\n".join(map("- {}".format, sorted(rcsetup._validators, key=str.lower))) -) class RcParams(MutableMapping, dict): """ A dict-like key-value store for config parameters, including validation. - Validating functions are defined and associated with rc parameters in - :mod:`matplotlib.rcsetup`. - - The list of rcParams is: - - %s + This is the data structure behind `matplotlib.rcParams`. - See Also - -------- - :ref:`customizing-with-matplotlibrc-files` + The complete list of rcParams can be found in :doc:`/users/explain/configuration`. """ validate = rcsetup._validators @@ -743,12 +748,11 @@ def __setitem__(self, key, val): and val is rcsetup._auto_backend_sentinel and "backend" in self): return + valid_key = _api.check_getitem( + self.validate, rcParam=key, _error_cls=KeyError + ) try: - cval = self.validate[key](val) - except KeyError as err: - raise KeyError( - f"{key} is not a valid rc parameter (see rcParams.keys() for " - f"a list of valid parameters)") from err + cval = valid_key(val) except ValueError as ve: raise ValueError(f"Key {key}: {ve}") from None self._set(key, cval) @@ -1021,7 +1025,7 @@ def rc(group, **kwargs): font = {'family' : 'monospace', 'weight' : 'bold', - 'size' : 'larger'} + 'size' : 'large'} rc('font', **font) # pass in the font dict as kwargs This enables you to easily switch between several configurations. Use @@ -1348,8 +1352,8 @@ def _init_tests(): def _replacer(data, value): """ - Either returns ``data[value]`` or passes ``data`` back, converts either to - a sequence. + Either returns ``data[value]`` or passes ``data`` back, converting any + ``MappingView`` to a sequence. """ try: # if key isn't a string don't bother diff --git a/lib/matplotlib/__init__.pyi b/lib/matplotlib/__init__.pyi index 07019109f406..321c5a4b90b2 100644 --- a/lib/matplotlib/__init__.pyi +++ b/lib/matplotlib/__init__.pyi @@ -39,7 +39,10 @@ import contextlib from packaging.version import Version from matplotlib._api import MatplotlibDeprecationWarning +from matplotlib.typing import RcKeyType, RcGroupKeyType from typing import Any, Literal, NamedTuple, overload +from matplotlib.typing import LogLevel + class _VersionInfo(NamedTuple): major: int @@ -52,7 +55,7 @@ __bibtex__: str __version__: str __version_info__: _VersionInfo -def set_loglevel(level: str) -> None: ... +def set_loglevel(level: LogLevel) -> None: ... class _ExecInfo(NamedTuple): executable: str @@ -67,18 +70,18 @@ def get_cachedir() -> str: ... def get_data_path() -> str: ... def matplotlib_fname() -> str: ... -class RcParams(dict[str, Any]): +class RcParams(dict[RcKeyType, Any]): validate: dict[str, Callable] def __init__(self, *args, **kwargs) -> None: ... - def _set(self, key: str, val: Any) -> None: ... - def _get(self, key: str) -> Any: ... + def _set(self, key: RcKeyType, val: Any) -> None: ... + def _get(self, key: RcKeyType) -> Any: ... def _update_raw(self, other_params: dict | RcParams) -> None: ... def _ensure_has_backend(self) -> None: ... - def __setitem__(self, key: str, val: Any) -> None: ... - def __getitem__(self, key: str) -> Any: ... - def __iter__(self) -> Generator[str, None, None]: ... + def __setitem__(self, key: RcKeyType, val: Any) -> None: ... + def __getitem__(self, key: RcKeyType) -> Any: ... + def __iter__(self) -> Generator[RcKeyType, None, None]: ... def __len__(self) -> int: ... def find_all(self, pattern: str) -> RcParams: ... def copy(self) -> RcParams: ... @@ -93,9 +96,9 @@ def rc_params_from_file( rcParamsDefault: RcParams rcParams: RcParams rcParamsOrig: RcParams -defaultParams: dict[str, Any] +defaultParams: dict[RcKeyType, Any] -def rc(group: str, **kwargs) -> None: ... +def rc(group: RcGroupKeyType, **kwargs) -> None: ... def rcdefaults() -> None: ... def rc_file_defaults() -> None: ... def rc_file( @@ -103,7 +106,7 @@ def rc_file( ) -> None: ... @contextlib.contextmanager def rc_context( - rc: dict[str, Any] | None = ..., fname: str | Path | os.PathLike | None = ... + rc: dict[RcKeyType, Any] | None = ..., fname: str | Path | os.PathLike | None = ... ) -> Generator[None, None, None]: ... def use(backend: str, *, force: bool = ...) -> None: ... @overload diff --git a/lib/matplotlib/_api/__init__.py b/lib/matplotlib/_api/__init__.py index 47c32f701729..216cc885ea24 100644 --- a/lib/matplotlib/_api/__init__.py +++ b/lib/matplotlib/_api/__init__.py @@ -10,6 +10,7 @@ """ +import difflib import functools import itertools import pathlib @@ -115,6 +116,9 @@ def check_in_list(values, /, *, _print_supported_values=True, **kwargs): ---------- values : iterable Sequence of values to check on. + + Note: All values must support == comparisons. + This means in particular the entries must not be numpy arrays. _print_supported_values : bool, default: True Whether to print *values* when raising ValueError. **kwargs : dict @@ -132,7 +136,18 @@ def check_in_list(values, /, *, _print_supported_values=True, **kwargs): if not kwargs: raise TypeError("No argument to check!") for key, val in kwargs.items(): - if val not in values: + try: + exists = val in values + except ValueError: + # `in` internally uses `val == values[i]`. There are some objects + # that do not support == to arbitrary other objects, in particular + # numpy arrays. + # Since such objects are not allowed in values, we can gracefully + # handle the case that val (typically provided by users) is of such + # type and directly state it's not in the list instead of letting + # the individual `val == values[i]` ValueError surface. + exists = False + if not exists: msg = f"{val!r} is not a valid value for {key}" if _print_supported_values: msg += f"; supported values are {', '.join(map(repr, values))}" @@ -174,12 +189,17 @@ def check_shape(shape, /, **kwargs): ) -def check_getitem(mapping, /, **kwargs): +def check_getitem(mapping, /, _error_cls=ValueError, **kwargs): """ *kwargs* must consist of a single *key, value* pair. If *key* is in *mapping*, return ``mapping[value]``; else, raise an appropriate ValueError. + Parameters + ---------- + _error_cls : + Class of error to raise. + Examples -------- >>> _api.check_getitem({"foo": "bar"}, arg=arg) @@ -190,9 +210,14 @@ def check_getitem(mapping, /, **kwargs): try: return mapping[v] except KeyError: - raise ValueError( - f"{v!r} is not a valid value for {k}; supported values are " - f"{', '.join(map(repr, mapping))}") from None + if len(mapping) > 5: + if len(best := difflib.get_close_matches(v, mapping.keys(), cutoff=0.5)): + suggestion = f"Did you mean one of {best}?" + else: + suggestion = "" + else: + suggestion = f"Supported values are {', '.join(map(repr, mapping))}" + raise _error_cls(f"{v!r} is not a valid value for {k}. {suggestion}") from None def caching_module_getattr(cls): diff --git a/lib/matplotlib/_api/__init__.pyi b/lib/matplotlib/_api/__init__.pyi index 9bf67110bb54..5db251c551e5 100644 --- a/lib/matplotlib/_api/__init__.pyi +++ b/lib/matplotlib/_api/__init__.pyi @@ -1,6 +1,6 @@ from collections.abc import Callable, Generator, Iterable, Mapping, Sequence from typing import Any, TypeVar, overload -from typing_extensions import Self # < Py 3.11 +from typing import Self from numpy.typing import NDArray @@ -42,7 +42,9 @@ def check_in_list( values: Sequence[Any], /, *, _print_supported_values: bool = ..., **kwargs: Any ) -> None: ... def check_shape(shape: tuple[int | None, ...], /, **kwargs: NDArray) -> None: ... -def check_getitem(mapping: Mapping[Any, Any], /, **kwargs: Any) -> Any: ... +def check_getitem( + mapping: Mapping[Any, _T], /, _error_cls: type[Exception], **kwargs: Any +) -> _T: ... def caching_module_getattr(cls: type) -> Callable[[str], Any]: ... @overload def define_aliases( diff --git a/lib/matplotlib/_api/deprecation.py b/lib/matplotlib/_api/deprecation.py index 65a754bbb43d..ce346e02e83d 100644 --- a/lib/matplotlib/_api/deprecation.py +++ b/lib/matplotlib/_api/deprecation.py @@ -419,6 +419,23 @@ def make_keyword_only(since, name, func=None): When used on a method that has a pyplot wrapper, this should be the outermost decorator, so that :file:`boilerplate.py` can access the original signature. + + Examples + -------- + Assume we want to only allow *dataset* and *positions* as positional + parameters on the method :: + + def violinplot(self, dataset, positions=None, vert=None, ...) + + Introduce the deprecation by adding the decorator :: + + @_api.make_keyword_only("3.10", "vert") + def violinplot(self, dataset, positions=None, vert=None, ...) + + When the deprecation expires, switch to :: + + def violinplot(self, dataset, positions=None, *, vert=None, ...) + """ decorator = functools.partial(make_keyword_only, since, name) diff --git a/lib/matplotlib/_cm.py b/lib/matplotlib/_cm.py index d3f4632108a8..cdad80bc9ddc 100644 --- a/lib/matplotlib/_cm.py +++ b/lib/matplotlib/_cm.py @@ -852,7 +852,6 @@ def _g36(x): return 2 * x - 1 (0.50196078431372548, 0.0 , 0.14901960784313725) ) - # ColorBrewer's qualitative maps, implemented using ListedColormap # for use with mpl.colors.NoNorm @@ -878,6 +877,22 @@ def _g36(x): return 2 * x - 1 (0.4, 0.4, 0.4 ), ) +# Okabe-Ito accessible and print-friendly color palette. +# By Masataka Okabe (Jikei Medical School) and Kei Ito (University of Tokyo). +# Qualitative color palette that is unambiguous regardless of whether +# the viewer has colorblindness. https://jfly.uni-koeln.de/color/#pallet + +_okabe_ito_data = ( + (0.0, 0.0, 0.0), # black + (0.9019607843137255, 0.6235294117647059, 0.0), # e69f00 + (0.33725490196078434, 0.7058823529411765, 0.9137254901960784), # 56b4e9 + (0.0, 0.6196078431372549, 0.45098039215686275), # 009e73 + (0.9411764705882353, 0.8941176470588236, 0.25882352941176473), # f0e442 + (0.0, 0.4470588235294118, 0.6980392156862745), # 0072b2 + (0.8352941176470589, 0.3686274509803922, 0.0), # d55e00 + (0.8, 0.4745098039215686, 0.6549019607843137), # cc79a7 +) + _Paired_data = ( (0.65098039215686276, 0.80784313725490198, 0.8901960784313725 ), (0.12156862745098039, 0.47058823529411764, 0.70588235294117652), @@ -1469,6 +1484,7 @@ def _gist_yarg(x): return 1 - x 'winter': _winter_data, # Qualitative 'Accent': {'listed': _Accent_data}, + 'okabe_ito': {'listed': _okabe_ito_data}, 'Dark2': {'listed': _Dark2_data}, 'Paired': {'listed': _Paired_data}, 'Pastel1': {'listed': _Pastel1_data}, diff --git a/lib/matplotlib/_cm_bivar.py b/lib/matplotlib/_cm_bivar.py index 53c0d48d7d6c..688e243accda 100644 --- a/lib/matplotlib/_cm_bivar.py +++ b/lib/matplotlib/_cm_bivar.py @@ -1,9 +1,8 @@ -# auto-generated by https://github.com/trygvrad/multivariate_colormaps -# date: 2024-05-24 - import numpy as np from matplotlib.colors import SegmentedBivarColormap +# auto-generated by https://github.com/trygvrad/multivariate_colormaps +# date: 2024-05-24 BiPeak = np.array( [0.000, 0.674, 0.931, 0.000, 0.680, 0.922, 0.000, 0.685, 0.914, 0.000, 0.691, 0.906, 0.000, 0.696, 0.898, 0.000, 0.701, 0.890, 0.000, 0.706, @@ -1276,32 +1275,9 @@ ]).reshape((65, 65, 3)) BiOrangeBlue = np.array( - [0.000, 0.000, 0.000, 0.000, 0.062, 0.125, 0.000, 0.125, 0.250, 0.000, - 0.188, 0.375, 0.000, 0.250, 0.500, 0.000, 0.312, 0.625, 0.000, 0.375, - 0.750, 0.000, 0.438, 0.875, 0.000, 0.500, 1.000, 0.125, 0.062, 0.000, - 0.125, 0.125, 0.125, 0.125, 0.188, 0.250, 0.125, 0.250, 0.375, 0.125, - 0.312, 0.500, 0.125, 0.375, 0.625, 0.125, 0.438, 0.750, 0.125, 0.500, - 0.875, 0.125, 0.562, 1.000, 0.250, 0.125, 0.000, 0.250, 0.188, 0.125, - 0.250, 0.250, 0.250, 0.250, 0.312, 0.375, 0.250, 0.375, 0.500, 0.250, - 0.438, 0.625, 0.250, 0.500, 0.750, 0.250, 0.562, 0.875, 0.250, 0.625, - 1.000, 0.375, 0.188, 0.000, 0.375, 0.250, 0.125, 0.375, 0.312, 0.250, - 0.375, 0.375, 0.375, 0.375, 0.438, 0.500, 0.375, 0.500, 0.625, 0.375, - 0.562, 0.750, 0.375, 0.625, 0.875, 0.375, 0.688, 1.000, 0.500, 0.250, - 0.000, 0.500, 0.312, 0.125, 0.500, 0.375, 0.250, 0.500, 0.438, 0.375, - 0.500, 0.500, 0.500, 0.500, 0.562, 0.625, 0.500, 0.625, 0.750, 0.500, - 0.688, 0.875, 0.500, 0.750, 1.000, 0.625, 0.312, 0.000, 0.625, 0.375, - 0.125, 0.625, 0.438, 0.250, 0.625, 0.500, 0.375, 0.625, 0.562, 0.500, - 0.625, 0.625, 0.625, 0.625, 0.688, 0.750, 0.625, 0.750, 0.875, 0.625, - 0.812, 1.000, 0.750, 0.375, 0.000, 0.750, 0.438, 0.125, 0.750, 0.500, - 0.250, 0.750, 0.562, 0.375, 0.750, 0.625, 0.500, 0.750, 0.688, 0.625, - 0.750, 0.750, 0.750, 0.750, 0.812, 0.875, 0.750, 0.875, 1.000, 0.875, - 0.438, 0.000, 0.875, 0.500, 0.125, 0.875, 0.562, 0.250, 0.875, 0.625, - 0.375, 0.875, 0.688, 0.500, 0.875, 0.750, 0.625, 0.875, 0.812, 0.750, - 0.875, 0.875, 0.875, 0.875, 0.938, 1.000, 1.000, 0.500, 0.000, 1.000, - 0.562, 0.125, 1.000, 0.625, 0.250, 1.000, 0.688, 0.375, 1.000, 0.750, - 0.500, 1.000, 0.812, 0.625, 1.000, 0.875, 0.750, 1.000, 0.938, 0.875, - 1.000, 1.000, 1.000, - ]).reshape((9, 9, 3)) + [0.0, 0.0, 0.0, 0.0, 0.5, 1.0, + 1.0, 0.5, 0.0, 1.0, 1.0, 1.0, + ]).reshape((2, 2, 3)) cmaps = { "BiPeak": SegmentedBivarColormap( diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index f5f23581bd9d..33ec8ef985e7 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -137,7 +137,8 @@ def do_constrained_layout(fig, h_pad, w_pad, layoutgrids[fig].update_variables() if check_no_collapsed_axes(layoutgrids, fig): reposition_axes(layoutgrids, fig, renderer, h_pad=h_pad, - w_pad=w_pad, hspace=hspace, wspace=wspace) + w_pad=w_pad, hspace=hspace, wspace=wspace, + compress=True) else: _api.warn_external(warn_collapsed) @@ -651,7 +652,7 @@ def get_pos_and_bbox(ax, renderer): def reposition_axes(layoutgrids, fig, renderer, *, - w_pad=0, h_pad=0, hspace=0, wspace=0): + w_pad=0, h_pad=0, hspace=0, wspace=0, compress=False): """ Reposition all the Axes based on the new inner bounding box. """ @@ -662,7 +663,7 @@ def reposition_axes(layoutgrids, fig, renderer, *, bbox=bbox.transformed(trans_fig_to_subfig)) reposition_axes(layoutgrids, sfig, renderer, w_pad=w_pad, h_pad=h_pad, - wspace=wspace, hspace=hspace) + wspace=wspace, hspace=hspace, compress=compress) for ax in fig._localaxes: if ax.get_subplotspec() is None or not ax.get_in_layout(): @@ -689,10 +690,10 @@ def reposition_axes(layoutgrids, fig, renderer, *, for nn, cbax in enumerate(ax._colorbars[::-1]): if ax == cbax._colorbar_info['parents'][0]: reposition_colorbar(layoutgrids, cbax, renderer, - offset=offset) + offset=offset, compress=compress) -def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None): +def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None, compress=False): """ Place the colorbar in its new place. @@ -706,6 +707,8 @@ def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None): offset : array-like Offset the colorbar needs to be pushed to in order to account for multiple colorbars. + compress : bool + Whether we're in compressed layout mode. """ parents = cbax._colorbar_info['parents'] @@ -724,6 +727,31 @@ def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None): aspect = cbax._colorbar_info['aspect'] shrink = cbax._colorbar_info['shrink'] + # For colorbars with a single parent in compressed layout, + # use the actual visual size of the parent axis after apply_aspect() + # has been called. This ensures colorbars align with their parent axes. + # This fix is specific to single-parent colorbars where alignment is critical. + if compress and len(parents) == 1: + from matplotlib.transforms import Bbox + # Get the actual parent position after apply_aspect() + parent_ax = parents[0] + actual_pos = parent_ax.get_position(original=False) + # Transform to figure coordinates + actual_pos_fig = actual_pos.transformed(fig.transSubfigure - fig.transFigure) + + if location in ('left', 'right'): + # For vertical colorbars, use the actual parent bbox height + # for colorbar sizing + # Keep the pb x-coordinates but use actual y-coordinates + pb = Bbox.from_extents(pb.x0, actual_pos_fig.y0, + pb.x1, actual_pos_fig.y1) + elif location in ('top', 'bottom'): + # For horizontal colorbars, use the actual parent bbox width + # for colorbar sizing + # Keep the pb y-coordinates but use actual x-coordinates + pb = Bbox.from_extents(actual_pos_fig.x0, pb.y0, + actual_pos_fig.x1, pb.y1) + cbpos, cbbbox = get_pos_and_bbox(cbax, renderer) # Colorbar gets put at extreme edge of outer bbox of the subplotspec diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 19ddbb6d0883..914cf7323b8b 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -8,6 +8,7 @@ import copy import enum import functools +import itertools import logging import math import os @@ -409,14 +410,14 @@ def get_xheight(self, fontname: str, fontsize: float, dpi: float) -> float: metrics = self.get_metrics( fontname, mpl.rcParams['mathtext.default'], 'x', fontsize, dpi) return metrics.iceberg - xHeight = (pclt['xHeight'] / 64.0) * (fontsize / 12.0) * (dpi / 100.0) - return xHeight + x_height = (pclt['xHeight'] / 64) * (fontsize / 12) * (dpi / 100) + return x_height def get_underline_thickness(self, font: str, fontsize: float, dpi: float) -> float: # This function used to grab underline thickness from the font # metrics, but that information is just too un-reliable, so it # is now hardcoded. - return ((0.75 / 12.0) * fontsize * dpi) / 72.0 + return ((0.75 / 12) * fontsize * dpi) / 72 def get_kern(self, font1: str, fontclass1: str, sym1: str, fontsize1: float, font2: str, fontclass2: str, sym2: str, fontsize2: float, @@ -569,7 +570,7 @@ def __init__(self, default_font_prop: FontProperties, load_glyph_flags: LoadFlag super().__init__(default_font_prop, load_glyph_flags) for texfont in "cal rm tt it bf sf bfit".split(): - prop = mpl.rcParams['mathtext.' + texfont] + prop = mpl.rcParams['mathtext.' + texfont] # type: ignore[index] font = findfont(prop) self.fontmap[texfont] = font prop = FontProperties('cmex10') @@ -1226,21 +1227,13 @@ def kern(self) -> None: linked list. """ new_children = [] - num_children = len(self.children) - if num_children: - for i in range(num_children): - elem = self.children[i] - if i < num_children - 1: - next = self.children[i + 1] - else: - next = None - - new_children.append(elem) - kerning_distance = elem.get_kerning(next) - if kerning_distance != 0.: - kern = Kern(kerning_distance) - new_children.append(kern) - self.children = new_children + for elem0, elem1 in itertools.zip_longest(self.children, self.children[1:]): + new_children.append(elem0) + kerning_distance = elem0.get_kerning(elem1) + if kerning_distance != 0.: + kern = Kern(kerning_distance) + new_children.append(kern) + self.children = new_children def hpack(self, w: float = 0.0, m: T.Literal['additional', 'exactly'] = 'additional') -> None: @@ -1534,11 +1527,9 @@ class AutoHeightChar(Hlist): def __init__(self, c: str, height: float, depth: float, state: ParserState, always: bool = False, factor: float | None = None): - alternatives = state.fontset.get_sized_alternatives_for_symbol( - state.font, c) + alternatives = state.fontset.get_sized_alternatives_for_symbol(state.font, c) - xHeight = state.fontset.get_xheight( - state.font, state.fontsize, state.dpi) + x_height = state.fontset.get_xheight(state.font, state.fontsize, state.dpi) state = state.copy() target_total = height + depth @@ -1546,8 +1537,8 @@ def __init__(self, c: str, height: float, depth: float, state: ParserState, state.font = fontname char = Char(sym, state) # Ensure that size 0 is chosen when the text is regular sized but - # with descender glyphs by subtracting 0.2 * xHeight - if char.height + char.depth >= target_total - 0.2 * xHeight: + # with descender glyphs by subtracting 0.2 * x_height + if char.height + char.depth >= target_total - 0.2 * x_height: break shift = 0.0 @@ -1574,8 +1565,7 @@ class AutoWidthChar(Hlist): def __init__(self, c: str, width: float, state: ParserState, always: bool = False, char_class: type[Char] = Char): - alternatives = state.fontset.get_sized_alternatives_for_symbol( - state.font, c) + alternatives = state.fontset.get_sized_alternatives_for_symbol(state.font, c) state = state.copy() for fontname, sym in alternatives: @@ -2468,7 +2458,7 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: state = self.get_state() rule_thickness = state.fontset.get_underline_thickness( state.font, state.fontsize, state.dpi) - xHeight = state.fontset.get_xheight( + x_height = state.fontset.get_xheight( state.font, state.fontsize, state.dpi) if napostrophes: @@ -2537,24 +2527,21 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: nucleus = Hlist([nucleus]) # Handle regular sub/superscripts - constants = _get_font_constant_set(state) + consts = _get_font_constant_set(state) lc_height = last_char.height lc_baseline = 0 if self.is_dropsub(last_char): lc_baseline = last_char.depth # Compute kerning for sub and super - superkern = constants.delta * xHeight - subkern = constants.delta * xHeight + superkern = consts.delta * x_height + subkern = consts.delta * x_height if self.is_slanted(last_char): - superkern += constants.delta * xHeight - superkern += (constants.delta_slanted * - (lc_height - xHeight * 2. / 3.)) + superkern += consts.delta * x_height + superkern += consts.delta_slanted * (lc_height - x_height * 2 / 3) if self.is_dropsub(last_char): - subkern = (3 * constants.delta - - constants.delta_integral) * lc_height - superkern = (3 * constants.delta + - constants.delta_integral) * lc_height + subkern = (3 * consts.delta - consts.delta_integral) * lc_height + superkern = (3 * consts.delta + consts.delta_integral) * lc_height else: subkern = 0 @@ -2567,28 +2554,28 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: x = Hlist([Kern(subkern), T.cast(Node, sub)]) x.shrink() if self.is_dropsub(last_char): - shift_down = lc_baseline + constants.subdrop * xHeight + shift_down = lc_baseline + consts.subdrop * x_height else: - shift_down = constants.sub1 * xHeight + shift_down = consts.sub1 * x_height x.shift_amount = shift_down else: x = Hlist([Kern(superkern), super]) x.shrink() if self.is_dropsub(last_char): - shift_up = lc_height - constants.subdrop * xHeight + shift_up = lc_height - consts.subdrop * x_height else: - shift_up = constants.sup1 * xHeight + shift_up = consts.sup1 * x_height if sub is None: x.shift_amount = -shift_up else: # Both sub and superscript y = Hlist([Kern(subkern), sub]) y.shrink() if self.is_dropsub(last_char): - shift_down = lc_baseline + constants.subdrop * xHeight + shift_down = lc_baseline + consts.subdrop * x_height else: - shift_down = constants.sub2 * xHeight + shift_down = consts.sub2 * x_height # If sub and superscript collide, move super up - clr = (2.0 * rule_thickness - + clr = (2 * rule_thickness - ((shift_up - x.depth) - (y.height - shift_down))) if clr > 0.: shift_up += clr @@ -2599,7 +2586,7 @@ def subsuper(self, s: str, loc: int, toks: ParseResults) -> T.Any: x.shift_amount = shift_down if not self.is_dropsub(last_char): - x.width += constants.script_space * xHeight + x.width += consts.script_space * x_height # Do we need to add a space after the nucleus? # To find out, check the flag set by operatorname @@ -2624,12 +2611,13 @@ def _genfrac(self, ldelim: str, rdelim: str, rule: float | None, style: _MathSty width = max(num.width, den.width) cnum.hpack(width, 'exactly') cden.hpack(width, 'exactly') - vlist = Vlist([cnum, # numerator - Vbox(0, thickness * 2.0), # space - Hrule(state, rule), # rule - Vbox(0, thickness * 2.0), # space - cden # denominator - ]) + vlist = Vlist([ + cnum, # numerator + Vbox(0, 2 * thickness), # space + Hrule(state, rule), # rule + Vbox(0, 2 * thickness), # space + cden, # denominator + ]) # Shift so the fraction line sits in the middle of the # equals sign @@ -2637,20 +2625,12 @@ def _genfrac(self, ldelim: str, rdelim: str, rule: float | None, style: _MathSty state.font, mpl.rcParams['mathtext.default'], '=', state.fontsize, state.dpi) shift = (cden.height - - ((metrics.ymax + metrics.ymin) / 2 - - thickness * 3.0)) + ((metrics.ymax + metrics.ymin) / 2 - 3 * thickness)) vlist.shift_amount = shift - result = [Hlist([vlist, Hbox(thickness * 2.)])] + result: list[Box | Char | str] = [Hlist([vlist, Hbox(2 * thickness)])] if ldelim or rdelim: - if ldelim == '': - ldelim = '.' - if rdelim == '': - rdelim = '.' - return self._auto_sized_delimiter(ldelim, - T.cast(list[Box | Char | str], - result), - rdelim) + return self._auto_sized_delimiter(ldelim or ".", result, rdelim or ".") return result def style_literal(self, toks: ParseResults) -> T.Any: @@ -2719,7 +2699,7 @@ def sqrt(self, toks: ParseResults) -> T.Any: # Determine the height of the body, and add a little extra to # the height so it doesn't seem cramped - height = body.height - body.shift_amount + thickness * 5.0 + height = body.height - body.shift_amount + 5 * thickness depth = body.depth + body.shift_amount check = AutoHeightChar(r'\__sqrt__', height, depth, state, always=True) height = check.height - check.shift_amount @@ -2729,13 +2709,13 @@ def sqrt(self, toks: ParseResults) -> T.Any: padded_body = Hlist([Hbox(2 * thickness), body, Hbox(2 * thickness)]) rightside = Vlist([Hrule(state), Glue('fill'), padded_body]) # Stretch the glue between the hrule and the body - rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0), + rightside.vpack(height + (state.fontsize * state.dpi) / (100 * 12), 'exactly', depth) # Add the root and shift it upward so it is above the tick. # The value of 0.6 is a hard-coded hack ;) if not root: - root = Box(check.width * 0.5, 0., 0.) + root = Box(0.5 * check.width, 0., 0.) else: root = Hlist(root) root.shrink() @@ -2744,11 +2724,12 @@ def sqrt(self, toks: ParseResults) -> T.Any: root_vlist = Vlist([Hlist([root])]) root_vlist.shift_amount = -height * 0.6 - hlist = Hlist([root_vlist, # Root - # Negative kerning to put root over tick - Kern(-check.width * 0.5), - check, # Check - rightside]) # Body + hlist = Hlist([ + root_vlist, # Root + Kern(-0.5 * check.width), # Negative kerning to put root over tick + check, # Check + rightside, # Body + ]) return [hlist] def overline(self, toks: ParseResults) -> T.Any: @@ -2757,14 +2738,14 @@ def overline(self, toks: ParseResults) -> T.Any: state = self.get_state() thickness = state.get_current_underline_thickness() - height = body.height - body.shift_amount + thickness * 3.0 + height = body.height - body.shift_amount + 3 * thickness depth = body.depth + body.shift_amount # Place overline above body rightside = Vlist([Hrule(state), Glue('fill'), Hlist([body])]) # Stretch the glue between the hrule and the body - rightside.vpack(height + (state.fontsize * state.dpi) / (100.0 * 12.0), + rightside.vpack(height + (state.fontsize * state.dpi) / (100 * 12), 'exactly', depth) hlist = Hlist([rightside]) @@ -2810,10 +2791,7 @@ def _auto_sized_delimiter(self, front: str, def auto_delim(self, toks: ParseResults) -> T.Any: return self._auto_sized_delimiter( - toks["left"], - # if "mid" in toks ... can be removed when requiring pyparsing 3. - toks["mid"].as_list() if "mid" in toks else [], - toks["right"]) + toks["left"], toks["mid"].as_list(), toks["right"]) def boldsymbol(self, toks: ParseResults) -> T.Any: self.push_state() diff --git a/lib/matplotlib/_pylab_helpers.py b/lib/matplotlib/_pylab_helpers.py index e3c3d98cb156..05f6d8aa02b3 100644 --- a/lib/matplotlib/_pylab_helpers.py +++ b/lib/matplotlib/_pylab_helpers.py @@ -53,6 +53,8 @@ def destroy(cls, num): two managers share the same number. """ if all(hasattr(num, attr) for attr in ["num", "destroy"]): + # num is a manager-like instance (not necessarily a + # FigureManagerBase subclass) manager = num if cls.figs.get(manager.num) is manager: cls.figs.pop(manager.num) diff --git a/lib/matplotlib/_style_helpers.py b/lib/matplotlib/_style_helpers.py new file mode 100644 index 000000000000..9b98d90593f9 --- /dev/null +++ b/lib/matplotlib/_style_helpers.py @@ -0,0 +1,51 @@ +import collections.abc +import itertools + +import numpy as np + +import matplotlib.cbook as cbook +import matplotlib.colors as mcolors +import matplotlib.lines as mlines + + +def check_non_empty(key, value): + """Raise a TypeError if an empty sequence is passed""" + if (not cbook.is_scalar_or_string(value) and + isinstance(value, collections.abc.Sized) and len(value) == 0): + raise TypeError(f'{key} must not be an empty sequence') + + +def style_generator(kw): + """ + Helper for handling style sequences (e.g. facecolor=['r', 'b', 'k']) within plotting + methods that repeatedly call other plotting methods (e.g. hist, stackplot). Remove + style keywords from the given dictionary. Return the reduced dictionary together + with a generator which provides a series of dictionaries to be used in each call to + the wrapped function. + """ + kw_iterators = {} + remaining_kw = {} + for key, value in kw.items(): + if key in ['facecolor', 'edgecolor']: + if value is None or cbook._str_lower_equal(value, 'none'): + kw_iterators[key] = itertools.repeat(value) + else: + check_non_empty(key, value) + kw_iterators[key] = itertools.cycle(mcolors.to_rgba_array(value)) + + elif key in ['hatch', 'linewidth']: + check_non_empty(key, value) + kw_iterators[key] = itertools.cycle(np.atleast_1d(value)) + + elif key == 'linestyle': + check_non_empty(key, value) + kw_iterators[key] = itertools.cycle(mlines._get_dash_patterns(value)) + + else: + remaining_kw[key] = value + + def style_gen(): + while True: + yield {key: next(val) for key, val in kw_iterators.items()} + + return remaining_kw, style_gen() diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 8756cb0c1439..be69dce4e927 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -612,6 +612,12 @@ class FFMpegFileWriter(FFMpegBase, FileMovieWriter): ``-framerate``, so see also `their notes on frame rates`_ for further details. .. _their notes on frame rates: https://trac.ffmpeg.org/wiki/Slideshow#Framerates + + Parameters + ---------- + *args, **kwargs + All arguments are forwarded to `FileMovieWriter`. See + `FileMovieWriter` for a list of all possible parameters. """ supported_formats = ['png', 'jpeg', 'tiff', 'raw', 'rgba'] @@ -861,7 +867,7 @@ class Animation: fig : `~matplotlib.figure.Figure` The figure object used to get needed events, such as draw or resize. - event_source : object, optional + event_source : object A class that can run a callback when desired events are generated, as well as be stopped and started. @@ -877,7 +883,7 @@ class Animation: FuncAnimation, ArtistAnimation """ - def __init__(self, fig, event_source=None, blit=False): + def __init__(self, fig, event_source, blit=False): self._draw_was_started = False self._fig = fig @@ -949,9 +955,21 @@ def save(self, filename, writer=None, fps=None, dpi=None, codec=None, filename : str The output filename, e.g., :file:`mymovie.mp4`. - writer : `MovieWriter` or str, default: :rc:`animation.writer` - A `MovieWriter` instance to use or a key that identifies a - class to use, such as 'ffmpeg'. + writer : `AbstractMovieWriter` subclass or str, default: :rc:`animation.writer` + The writer used to grab the frames and create the movie file. + This can be an instance of an `AbstractMovieWriter` subclass or a + string. The builtin writers are + + ================== ============================== + str class + ================== ============================== + 'ffmpeg' `.FFMpegWriter` + 'ffmpeg_file' `.FFMpegFileWriter` + 'imagemagick' `.ImageMagickWriter` + 'imagemagick_file' `.ImageMagickFileWriter` + 'pillow' `.PillowWriter` + 'html' `.HTMLWriter` + ================== ============================== fps : int, optional Movie frame rate (per second). If not set, the frame rate from the diff --git a/lib/matplotlib/animation.pyi b/lib/matplotlib/animation.pyi index f725df8ebb22..e90a0103aefd 100644 --- a/lib/matplotlib/animation.pyi +++ b/lib/matplotlib/animation.pyi @@ -6,7 +6,7 @@ from matplotlib.artist import Artist from matplotlib.backend_bases import TimerBase from matplotlib.figure import Figure -from typing import Any +from typing import Any, Protocol subprocess_creation_flags: int @@ -152,11 +152,17 @@ class HTMLWriter(FileMovieWriter): def grab_frame(self, **savefig_kwargs): ... def finish(self) -> None: ... +class EventSourceProtocol(Protocol): + def add_callback(self, func: Callable): ... + def remove_callback(self, func: Callable): ... + def start(self): ... + def stop(self): ... + class Animation: frame_seq: Iterable[Artist] - event_source: Any + event_source: EventSourceProtocol | None # TODO: We should remove None def __init__( - self, fig: Figure, event_source: Any | None = ..., blit: bool = ... + self, fig: Figure, event_source: EventSourceProtocol, blit: bool = ... ) -> None: ... def __del__(self) -> None: ... def save( diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index eaaae43e283a..79a629e9f4c6 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -165,10 +165,24 @@ def _update_set_signature_and_docstring(cls): if prop not in Artist._PROPERTIES_EXCLUDED_FROM_SET]]) cls.set._autogenerated_signature = True - cls.set.__doc__ = ( - "Set multiple properties at once.\n\n" - "Supported properties are\n\n" - + kwdoc(cls)) + cls.set.__doc__ = ("""\ +Set multiple properties at once. + +:: + a.set(a=A, b=B, c=C) + +is equivalent to :: + + a.set_a(A) + a.set_b(B) + a.set_c(C) + +The order of operations is not guaranteed, however most properties do not +depend on each other. + +Supported properties are + +""" + kwdoc(cls)) def __init__(self): self._stale = True diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 3e39bbd4acdc..2bdb6ffd6a3f 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -32,11 +32,13 @@ import matplotlib.transforms as mtransforms import matplotlib.tri as mtri import matplotlib.units as munits -from matplotlib import _api, _docstring, _preprocess_data +from matplotlib import _api, _docstring, _preprocess_data, _style_helpers from matplotlib.axes._base import ( _AxesBase, _TransformedBoundsLocator, _process_plot_format) from matplotlib.axes._secondary_axes import SecondaryAxis -from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer +from matplotlib.container import ( + BarContainer, ErrorbarContainer, PieContainer, StemContainer) +from matplotlib.text import Text from matplotlib.transforms import _ScaledRotation _log = logging.getLogger(__name__) @@ -1183,7 +1185,7 @@ def hlines(self, y, xmin, xmax, colors=None, linestyles='solid', if self.name == "rectilinear": datalim = lines.get_datalim(self.transData) t = lines.get_transform() - updatex, updatey = t.contains_branch_seperately(self.transData) + updatex, updatey = t.contains_branch_separately(self.transData) minx = np.nanmin(datalim.xmin) maxx = np.nanmax(datalim.xmax) miny = np.nanmin(datalim.ymin) @@ -1275,7 +1277,7 @@ def vlines(self, x, ymin, ymax, colors=None, linestyles='solid', if self.name == "rectilinear": datalim = lines.get_datalim(self.transData) t = lines.get_transform() - updatex, updatey = t.contains_branch_seperately(self.transData) + updatex, updatey = t.contains_branch_separately(self.transData) minx = np.nanmin(datalim.xmin) maxx = np.nanmax(datalim.xmax) miny = np.nanmin(datalim.ymin) @@ -2966,7 +2968,7 @@ def sign(x): @_preprocess_data() @_docstring.interpd - def broken_barh(self, xranges, yrange, **kwargs): + def broken_barh(self, xranges, yrange, align="bottom", **kwargs): """ Plot a horizontal sequence of rectangles. @@ -2979,8 +2981,16 @@ def broken_barh(self, xranges, yrange, **kwargs): The x-positions and extents of the rectangles. For each tuple (*xmin*, *xwidth*) a rectangle is drawn from *xmin* to *xmin* + *xwidth*. - yrange : (*ymin*, *yheight*) + yrange : (*ypos*, *yheight*) The y-position and extent for all the rectangles. + align : {"bottom", "center", "top"}, default: 'bottom' + The alignment of the yrange with respect to the y-position. One of: + + - "bottom": Resulting y-range [ypos, ypos + yheight] + - "center": Resulting y-range [ypos - yheight/2, ypos + yheight/2] + - "top": Resulting y-range [ypos - yheight, ypos] + + .. versionadded:: 3.11 Returns ------- @@ -3015,7 +3025,15 @@ def broken_barh(self, xranges, yrange, **kwargs): vertices = [] y0, dy = yrange - y0, y1 = self.convert_yunits((y0, y0 + dy)) + + _api.check_in_list(['bottom', 'center', 'top'], align=align) + if align == "bottom": + y0, y1 = self.convert_yunits((y0, y0 + dy)) + elif align == "center": + y0, y1 = self.convert_yunits((y0 - dy/2, y0 + dy/2)) + else: + y0, y1 = self.convert_yunits((y0 - dy, y0)) + for xr in xranges: # convert the absolute values, not the x and dx try: x0, dx = xr @@ -3027,8 +3045,7 @@ def broken_barh(self, xranges, yrange, **kwargs): vertices.append([(x0, y0), (x0, y1), (x1, y1), (x1, y0)]) col = mcoll.PolyCollection(np.array(vertices), **kwargs) - self.add_collection(col, autolim=True) - self._request_autoscale_view() + self.add_collection(col) return col @@ -3177,6 +3194,16 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing **kwargs : `.Rectangle` properties + Properties applied to all bars. The following properties additionally + accept a sequence of values corresponding to the datasets in + *heights*: + + - *edgecolor* + - *facecolor* + - *linewidth* + - *linestyle* + - *hatch* + %(Rectangle:kwdoc)s Returns @@ -3303,6 +3330,8 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing # TODO: do we want to be more restrictive and check lengths? colors = itertools.cycle(colors) + kwargs, style_gen = _style_helpers.style_generator(kwargs) + bar_width = (group_distance / (num_datasets + (num_datasets - 1) * bar_spacing + group_spacing)) bar_spacing_abs = bar_spacing * bar_width @@ -3316,15 +3345,16 @@ def grouped_bar(self, heights, *, positions=None, group_spacing=1.5, bar_spacing # place the bars, but only use numerical positions, categorical tick labels # are handled separately below bar_containers = [] - for i, (hs, label, color) in enumerate(zip(heights, labels, colors)): + for i, (hs, label, color, styles) in enumerate(zip(heights, labels, colors, + style_gen)): lefts = (group_centers - 0.5 * group_distance + margin_abs + i * (bar_width + bar_spacing_abs)) if orientation == "vertical": bc = self.bar(lefts, hs, width=bar_width, align="edge", - label=label, color=color, **kwargs) + label=label, color=color, **styles, **kwargs) else: bc = self.barh(lefts, hs, height=bar_width, align="edge", - label=label, color=color, **kwargs) + label=label, color=color, **styles, **kwargs) bar_containers.append(bc) if tick_labels is not None: @@ -3579,7 +3609,7 @@ def pie(self, x, explode=None, labels=None, colors=None, keywords, properties passed to *wedgeprops* take precedence. textprops : dict, default: None - Dict of arguments to pass to the text objects. + Dict of arguments to pass to the `.Text` objects. center : (float, float), default: (0, 0) The coordinates of the center of the chart. @@ -3600,15 +3630,11 @@ def pie(self, x, explode=None, labels=None, colors=None, Returns ------- - patches : list - A sequence of `matplotlib.patches.Wedge` instances + `.PieContainer` + Container with all the wedge patches and any associated text objects. - texts : list - A list of the label `.Text` instances. - - autotexts : list - A list of `.Text` instances for the numeric labels. This will only - be returned if the parameter *autopct* is not *None*. + .. versionchanged:: 3.11 + Previously the wedges and texts were returned in a tuple. Notes ----- @@ -3618,9 +3644,7 @@ def pie(self, x, explode=None, labels=None, colors=None, The Axes aspect ratio can be controlled with `.Axes.set_aspect`. """ self.set_aspect('equal') - # The use of float32 is "historical", but can't be changed without - # regenerating the test baselines. - x = np.asarray(x, np.float32) + x = np.asarray(x) if x.ndim > 1: raise ValueError("x must be 1D") @@ -3636,9 +3660,11 @@ def pie(self, x, explode=None, labels=None, colors=None, raise ValueError('All wedge sizes are zero') if normalize: - x = x / sx + fracs = x / sx elif sx > 1: raise ValueError('Cannot plot an unnormalized pie with sum(x) > 1') + else: + fracs = x if labels is None: labels = [''] * len(x) if explode is None: @@ -3666,21 +3692,17 @@ def get_next_color(): if wedgeprops is None: wedgeprops = {} - if textprops is None: - textprops = {} - texts = [] slices = [] - autotexts = [] - for frac, label, expl in zip(x, labels, explode): - x, y = center + for frac, label, expl in zip(fracs, labels, explode): + x_pos, y_pos = center theta2 = (theta1 + frac) if counterclock else (theta1 - frac) thetam = 2 * np.pi * 0.5 * (theta1 + theta2) - x += expl * math.cos(thetam) - y += expl * math.sin(thetam) + x_pos += expl * math.cos(thetam) + y_pos += expl * math.sin(thetam) - w = mpatches.Wedge((x, y), radius, 360. * min(theta1, theta2), + w = mpatches.Wedge((x_pos, y_pos), radius, 360. * min(theta1, theta2), 360. * max(theta1, theta2), facecolor=get_next_color(), hatch=next(hatch_cycle), @@ -3698,28 +3720,28 @@ def get_next_color(): shadow_dict.update(shadow) self.add_patch(mpatches.Shadow(w, **shadow_dict)) - if labeldistance is not None: - xt = x + labeldistance * radius * math.cos(thetam) - yt = y + labeldistance * radius * math.sin(thetam) - label_alignment_h = 'left' if xt > 0 else 'right' - label_alignment_v = 'center' - label_rotation = 'horizontal' - if rotatelabels: - label_alignment_v = 'bottom' if yt > 0 else 'top' - label_rotation = (np.rad2deg(thetam) - + (0 if xt > 0 else 180)) - t = self.text(xt, yt, label, - clip_on=False, - horizontalalignment=label_alignment_h, - verticalalignment=label_alignment_v, - rotation=label_rotation, - size=mpl.rcParams['xtick.labelsize']) - t.set(**textprops) - texts.append(t) - - if autopct is not None: - xt = x + pctdistance * radius * math.cos(thetam) - yt = y + pctdistance * radius * math.sin(thetam) + theta1 = theta2 + + pc = PieContainer(slices, x, normalize) + + if labeldistance is None: + # Insert an empty list of texts for backwards compatibility of the + # return value. + pc.add_texts([]) + else: + # Add labels to the wedges. + labels_textprops = { + 'fontsize': mpl.rcParams['xtick.labelsize'], + **cbook.normalize_kwargs(textprops or {}, Text) + } + self.pie_label(pc, labels, distance=labeldistance, + alignment='outer', rotate=rotatelabels, + textprops=labels_textprops) + + if autopct is not None: + # Add automatic percentage labels to wedges + auto_labels = [] + for frac in fracs: if isinstance(autopct, str): s = autopct % (100. * frac) elif callable(autopct): @@ -3727,17 +3749,15 @@ def get_next_color(): else: raise TypeError( 'autopct must be callable or a format string') - if mpl._val_or_rc(textprops.get("usetex"), "text.usetex"): + if textprops is not None and mpl._val_or_rc(textprops.get("usetex"), + "text.usetex"): # escape % (i.e. \%) if it is not already escaped s = re.sub(r"([^\\])%", r"\1\\%", s) - t = self.text(xt, yt, s, - clip_on=False, - horizontalalignment='center', - verticalalignment='center') - t.set(**textprops) - autotexts.append(t) + auto_labels.append(s) - theta1 = theta2 + self.pie_label(pc, auto_labels, distance=pctdistance, + alignment='center', + textprops=textprops) if frame: self._request_autoscale_view() @@ -3746,10 +3766,107 @@ def get_next_color(): xlim=(-1.25 + center[0], 1.25 + center[0]), ylim=(-1.25 + center[1], 1.25 + center[1])) - if autopct is None: - return slices, texts - else: - return slices, texts, autotexts + return pc + + def pie_label(self, container, /, labels, *, distance=0.6, + textprops=None, rotate=False, alignment='auto'): + """ + Label a pie chart. + + .. versionadded:: 3.11 + + Adds labels to wedges in the given `.PieContainer`. + + Parameters + ---------- + container : `.PieContainer` + Container with all the wedges, likely returned from `.pie`. + + labels : str or list of str + A sequence of strings providing the labels for each wedge, or a format + string with ``absval`` and/or ``frac`` placeholders. For example, to label + each wedge with its value and the percentage in brackets:: + + wedge_labels="{absval:d} ({frac:.0%})" + + distance : float, default: 0.6 + The radial position of the labels, relative to the pie radius. Values > 1 + are outside the wedge and values < 1 are inside the wedge. + + textprops : dict, default: None + Dict of arguments to pass to the `.Text` objects. + + rotate : bool, default: False + Rotate each label to the angle of the corresponding slice if true. + + alignment : {'center', 'outer', 'auto'}, default: 'auto' + Controls the horizontal alignment of the text objects relative to their + nominal position. + + - 'center': The labels are centered on their points. + - 'outer': Labels are aligned away from the center of the pie, i.e., labels + on the left side of the pie are right-aligned and labels on the right + side are left-aligned. + - 'auto': Translates to 'outer' if *distance* > 1 (so that the labels do not + overlap the wedges) and 'center' if *distance* < 1. + + If *rotate* is True, the vertical alignment is also affected in an + analogous way. + + - 'center': The labels are centered on their points. + - 'outer': Labels are aligned away from the center of the pie, i.e., labels + on the top half of the pie are bottom-aligned and labels on the bottom + half are top-aligned. + + Returns + ------- + list + A list of the label `.Text` instances. + """ + _api.check_in_list(['center', 'outer', 'auto'], alignment=alignment) + if alignment == 'auto': + alignment = 'outer' if distance > 1 else 'center' + + if textprops is None: + textprops = {} + + if isinstance(labels, str): + # Assume we have a format string + labels = [labels.format(absval=val, frac=frac) for val, frac in + zip(container.values, container.fracs)] + if mpl._val_or_rc(textprops.get("usetex"), "text.usetex"): + # escape % (i.e. \%) if it is not already escaped + labels = [re.sub(r"([^\\])%", r"\1\\%", s) for s in labels] + elif (nw := len(container.wedges)) != (nl := len(labels)): + raise ValueError( + f'The number of labels ({nl}) must match the number of wedges ({nw})') + + texts = [] + + for wedge, label in zip(container.wedges, labels): + thetam = 2 * np.pi * 0.5 * (wedge.theta1 + wedge.theta2) / 360 + xt = wedge.center[0] + distance * wedge.r * math.cos(thetam) + yt = wedge.center[1] + distance * wedge.r * math.sin(thetam) + if alignment == 'outer': + label_alignment_h = 'left' if xt > 0 else 'right' + else: + label_alignment_h = 'center' + label_alignment_v = 'center' + label_rotation = 'horizontal' + if rotate: + if alignment == 'outer': + label_alignment_v = 'bottom' if yt > 0 else 'top' + label_rotation = (np.rad2deg(thetam) + (0 if xt > 0 else 180)) + t = self.text(xt, yt, label, clip_on=False, rotation=label_rotation, + horizontalalignment=label_alignment_h, + verticalalignment=label_alignment_v) + t.set(**textprops) + texts.append(t) + + container.add_texts(texts) + + return texts + @staticmethod def _errorevery_to_mask(x, errorevery): @@ -4208,10 +4325,12 @@ def boxplot(self, x, notch=None, sym=None, vert=None, Parameters ---------- - x : Array or a sequence of vectors. - The input data. If a 2D array, a boxplot is drawn for each column - in *x*. If a sequence of 1D arrays, a boxplot is drawn for each - array in *x*. + x : 1D array or sequence of 1D arrays or 2D array + The input data. Possible values: + + - 1D array: A single box is drawn. + - sequence of 1D arrays: A box is drawn for each array in the sequence. + - 2D array: A box is drawn for each column in the array. notch : bool, default: :rc:`boxplot.notch` Whether to draw a notched boxplot (`True`), or a rectangular @@ -5205,12 +5324,12 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, s = (20 if mpl.rcParams['_internal.classic_mode'] else mpl.rcParams['lines.markersize'] ** 2.0) s = np.ma.ravel(s) - if (len(s) not in (1, x.size) or - (not np.issubdtype(s.dtype, np.floating) and - not np.issubdtype(s.dtype, np.integer))): - raise ValueError( - "s must be a scalar, " - "or float array-like with the same size as x and y") + if not (np.issubdtype(s.dtype, np.floating) + or np.issubdtype(s.dtype, np.integer)): + raise ValueError(f"s must be float, but has type {s.dtype}") + if len(s) not in (1, x.size): + raise ValueError(f"s (size {len(s)}) cannot be broadcast " + f"to match x and y (size {len(x)})") # get the original edgecolor the user passed before we normalize orig_edgecolor = edgecolors @@ -5256,7 +5375,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, if not marker_obj.is_filled(): if orig_edgecolor is not None: _api.warn_external( - f"You passed a edgecolor/edgecolors ({orig_edgecolor!r}) " + f"You passed an edgecolor/edgecolors ({orig_edgecolor!r}) " f"for an unfilled marker ({marker!r}). Matplotlib is " "ignoring the edgecolor in favor of the facecolor. This " "behavior may change in the future." @@ -5337,7 +5456,6 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, self.set_ymargin(0.05) self.add_collection(collection) - self._request_autoscale_view() return collection @@ -5406,8 +5524,9 @@ def hexbin(self, x, y, C=None, gridsize=100, bins=None, - If *None*, no binning is applied; the color of each hexagon directly corresponds to its count value. - If 'log', use a logarithmic scale for the colormap. - Internally, :math:`log_{10}(i+1)` is used to determine the + Internally, :math:`log_{10}(i)` is used to determine the hexagon color. This is equivalent to ``norm=LogNorm()``. + Note that 0 counts are thus marked with the "bad" color. - If an integer, divide the counts in the specified number of bins, and color the hexagons accordingly. - If a sequence of values, the values of the lower bound of @@ -5545,8 +5664,8 @@ def reduce_C_function(C: array) -> float ymin, ymax = (ty.min(), ty.max()) if len(y) else (0, 1) # to avoid issues with singular data, expand the min/max pairs - xmin, xmax = mtransforms.nonsingular(xmin, xmax, expander=0.1) - ymin, ymax = mtransforms.nonsingular(ymin, ymax, expander=0.1) + xmin, xmax = mtransforms._nonsingular(xmin, xmax, expander=0.1) + ymin, ymax = mtransforms._nonsingular(ymin, ymax, expander=0.1) nx1 = nx + 1 ny1 = ny + 1 @@ -5807,8 +5926,7 @@ def quiver(self, *args, **kwargs): # Make sure units are handled for x and y values args = self._quiver_units(args, kwargs) q = mquiver.Quiver(self, *args, **kwargs) - self.add_collection(q, autolim=True) - self._request_autoscale_view() + self.add_collection(q) return q # args can be some combination of X, Y, U, V, C and all should be replaced @@ -5819,8 +5937,7 @@ def barbs(self, *args, **kwargs): # Make sure units are handled for x and y values args = self._quiver_units(args, kwargs) b = mquiver.Barbs(self, *args, **kwargs) - self.add_collection(b, autolim=True) - self._request_autoscale_view() + self.add_collection(b) return b # Uses a custom implementation of data-kwarg handling in @@ -5980,7 +6097,6 @@ def _fill_between_x_or_y( where=where, interpolate=interpolate, step=step, **kwargs) self.add_collection(collection) - self._request_autoscale_view() return collection def _fill_between_process_units(self, ind_dir, dep_dir, ind, dep1, dep2, **kwargs): @@ -6691,9 +6807,12 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, See :doc:`/gallery/images_contours_and_fields/pcolormesh_grids` for more description. - snap : bool, default: False + snap : bool, default: :rc:`pcolormesh.snap` Whether to snap the mesh to pixel boundaries. + .. versionchanged:: 3.4.0 + The default value changed from *False* to *True* to improve transparency + handling. See :ref:`whats-new-3-4-0` for details. rasterized : bool, optional Rasterize the pcolormesh when drawing vector graphics. This can speed up rendering and produce smaller files for large data sets. @@ -6798,7 +6917,7 @@ def _update_pcolor_lims(self, collection, coords): hasattr(t, '_as_mpl_transform')): t = t._as_mpl_transform(self.axes) - if t and any(t.contains_branch_seperately(self.transData)): + if t and any(t.contains_branch_separately(self.transData)): trans_to_data = t - self.transData coords = trans_to_data.transform(coords) @@ -7212,6 +7331,9 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, Color or sequence of colors, one per dataset. Default (``None``) uses the standard line color sequence. + .. versionadded:: 3.10 + It is now possible to use a single color with multiple datasets. + label : str or list of str, optional String, or sequence of strings to match multiple datasets. Bar charts yield multiple patches per dataset, but only the first gets @@ -7333,6 +7455,8 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, if color is None: colors = [self._get_lines.get_next_color() for i in range(nx)] else: + if mcolors.is_color_like(color): + color = [color]*nx colors = mcolors.to_rgba_array(color) if len(colors) != nx: raise ValueError(f"The 'color' keyword argument must have one " @@ -7531,38 +7655,15 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, labels = [] if label is None else np.atleast_1d(np.asarray(label, str)) if histtype == "step": - ec = kwargs.get('edgecolor', colors) - else: - ec = kwargs.get('edgecolor', None) - if ec is None or cbook._str_lower_equal(ec, 'none'): - edgecolors = itertools.repeat(ec) - else: - edgecolors = itertools.cycle(mcolors.to_rgba_array(ec)) + kwargs.setdefault('edgecolor', colors) - fc = kwargs.get('facecolor', colors) - if cbook._str_lower_equal(fc, 'none'): - facecolors = itertools.repeat(fc) - else: - facecolors = itertools.cycle(mcolors.to_rgba_array(fc)) - - hatches = itertools.cycle(np.atleast_1d(kwargs.get('hatch', None))) - linewidths = itertools.cycle(np.atleast_1d(kwargs.get('linewidth', None))) - if 'linestyle' in kwargs: - linestyles = itertools.cycle(mlines._get_dash_patterns(kwargs['linestyle'])) - else: - linestyles = itertools.repeat(None) + kwargs, style_gen = _style_helpers.style_generator(kwargs) for patch, lbl in itertools.zip_longest(patches, labels): if not patch: continue p = patch[0] - kwargs.update({ - 'hatch': next(hatches), - 'linewidth': next(linewidths), - 'linestyle': next(linestyles), - 'edgecolor': next(edgecolors), - 'facecolor': next(facecolors), - }) + kwargs.update(next(style_gen)) p._internal_update(kwargs) if lbl is not None: p.set_label(lbl) @@ -7761,13 +7862,15 @@ def hist2d(self, x, y, bins=10, range=None, density=False, weights=None, Notes ----- - - Currently ``hist2d`` calculates its own axis limits, and any limits - previously set are ignored. - - Rendering the histogram with a logarithmic color scale is - accomplished by passing a `.colors.LogNorm` instance to the *norm* - keyword argument. Likewise, power-law normalization (similar - in effect to gamma correction) can be accomplished with - `.colors.PowerNorm`. + Rendering the histogram with a logarithmic color scale is accomplished + by passing a `.colors.LogNorm` instance to the *norm* keyword + argument. Likewise, power-law normalization (similar in effect to gamma + correction) can be accomplished with `.colors.PowerNorm`. + + .. versionchanged:: 3.11 + Previously, `~.Axes.hist2d` would force the axes limits to match the + extents of the histogram; now, autoscaling also takes other plot + elements into account. """ h, xedges, yedges = np.histogram2d(x, y, bins=bins, range=range, @@ -7779,8 +7882,6 @@ def hist2d(self, x, y, bins=10, range=None, density=False, weights=None, h[h > cmax] = None pc = self.pcolormesh(xedges, yedges, h.T, **kwargs) - self.set_xlim(xedges[0], xedges[-1]) - self.set_ylim(yedges[0], yedges[-1]) return h, xedges, yedges, pc @@ -8763,8 +8864,12 @@ def violinplot(self, dataset, positions=None, vert=None, Parameters ---------- - dataset : Array or a sequence of vectors. - The input data. + dataset : 1D array or sequence of 1D arrays or 2D array + The input data. Possible values: + + - 1D array: A single violin is drawn. + - sequence of 1D arrays: A violin is drawn for each array in the sequence. + - 2D array: A violin is drawn for each column in the array. positions : array-like, default: [1, 2, ..., n] The positions of the violins; i.e. coordinates on the x-axis for @@ -8867,18 +8972,8 @@ def violinplot(self, dataset, positions=None, vert=None, .Axes.violin : Draw a violin from pre-computed statistics. boxplot : Draw a box and whisker plot. """ - - def _kde_method(X, coords): - # Unpack in case of e.g. Pandas or xarray object - X = cbook._unpack_to_numpy(X) - # fallback gracefully if the vector contains only one value - if np.all(X[0] == X): - return (X[0] == coords).astype(float) - kde = mlab.GaussianKDE(X, bw_method) - return kde.evaluate(coords) - - vpstats = cbook.violin_stats(dataset, _kde_method, points=points, - quantiles=quantiles) + vpstats = cbook.violin_stats(dataset, ("GaussianKDE", bw_method), + points=points, quantiles=quantiles) return self.violin(vpstats, positions=positions, vert=vert, orientation=orientation, widths=widths, showmeans=showmeans, showextrema=showextrema, @@ -8966,6 +9061,14 @@ def violin(self, vpstats, positions=None, vert=None, .. versionadded:: 3.11 + For backward compatibility, if *facecolor* is not given, the body + will get an Artist-level transparency `alpha <.Artist.set_alpha>` + of 0.3, which will persist if you afterwards change the facecolor, + e.g. via ``result['bodies'][0].set_facecolor('red')``. + If *facecolor* is given, there is no Artist-level transparency. + To set transparency for *facecolor* or *edgecolor* use + ``(color, alpha)`` tuples. + linecolor : :mpltype:`color` or list of :mpltype:`color`, optional If provided, will set the line color(s) of the violins (the horizontal and vertical spines and body edges). @@ -9005,6 +9108,8 @@ def violin(self, vpstats, positions=None, vert=None, -------- violinplot : Draw a violin plot from data instead of pre-computed statistics. + .cbook.violin_stats: + Calculate a *vpstats* dictionary from data, suitable for passing to violin. """ # Statistical quantities to be plotted on the violins @@ -9071,13 +9176,14 @@ def cycle_color(color, alpha=None): if facecolor is not None: facecolor = cycle_color(facecolor) + body_artist_alpha = None else: - default_facealpha = 0.3 + body_artist_alpha = 0.3 # Use default colors if user doesn't provide them if mpl.rcParams['_internal.classic_mode']: - facecolor = cycle_color('y', alpha=default_facealpha) + facecolor = cycle_color('y') else: - facecolor = cycle_color(next_color, alpha=default_facealpha) + facecolor = cycle_color(next_color) if mpl.rcParams['_internal.classic_mode']: # Classic mode uses patch.force_edgecolor=True, so we need to @@ -9126,7 +9232,8 @@ def cycle_color(color, alpha=None): bodies += [fill(stats['coords'], -vals + pos if side in ['both', 'low'] else pos, vals + pos if side in ['both', 'high'] else pos, - facecolor=facecolor, edgecolor=body_edgecolor)] + facecolor=facecolor, edgecolor=body_edgecolor, + alpha=body_artist_alpha)] means.append(stats['mean']) mins.append(stats['min']) maxes.append(stats['max']) diff --git a/lib/matplotlib/axes/_axes.pyi b/lib/matplotlib/axes/_axes.pyi index 0008363b8220..09587ab753a3 100644 --- a/lib/matplotlib/axes/_axes.pyi +++ b/lib/matplotlib/axes/_axes.pyi @@ -13,7 +13,8 @@ from matplotlib.collections import ( ) from matplotlib.colorizer import Colorizer from matplotlib.colors import Colormap, Normalize -from matplotlib.container import BarContainer, ErrorbarContainer, StemContainer +from matplotlib.container import ( + BarContainer, PieContainer, ErrorbarContainer, StemContainer) from matplotlib.contour import ContourSet, QuadContourSet from matplotlib.image import AxesImage, PcolorImage from matplotlib.inset import InsetIndicator @@ -21,7 +22,7 @@ from matplotlib.legend import Legend from matplotlib.legend_handler import HandlerBase from matplotlib.lines import Line2D, AxLine from matplotlib.mlab import GaussianKDE -from matplotlib.patches import Rectangle, FancyArrow, Polygon, StepPatch, Wedge +from matplotlib.patches import Rectangle, FancyArrow, Polygon, StepPatch from matplotlib.quiver import Quiver, QuiverKey, Barbs from matplotlib.text import Annotation, Text from matplotlib.transforms import Transform @@ -36,7 +37,7 @@ from collections.abc import Callable, Iterable, Sequence from typing import Any, Literal, overload import numpy as np from numpy.typing import ArrayLike -from matplotlib.typing import ColorType, MarkerType, LineStyleType +from matplotlib.typing import ColorType, MarkerType, LegendLocType, LineStyleType import pandas as pd @@ -65,13 +66,16 @@ class Axes(_AxesBase): @overload def legend(self) -> Legend: ... @overload - def legend(self, handles: Iterable[Artist | tuple[Artist, ...]], labels: Iterable[str], **kwargs) -> Legend: ... + def legend(self, handles: Iterable[Artist | tuple[Artist, ...]], labels: Iterable[str], + *, loc: LegendLocType | None = ..., **kwargs) -> Legend: ... @overload - def legend(self, *, handles: Iterable[Artist | tuple[Artist, ...]], **kwargs) -> Legend: ... + def legend(self, *, handles: Iterable[Artist | tuple[Artist, ...]], + loc: LegendLocType | None = ..., **kwargs) -> Legend: ... @overload - def legend(self, labels: Iterable[str], **kwargs) -> Legend: ... + def legend(self, labels: Iterable[str], + *, loc: LegendLocType | None = ..., **kwargs) -> Legend: ... @overload - def legend(self, **kwargs) -> Legend: ... + def legend(self, *, loc: LegendLocType | None = ..., **kwargs) -> Legend: ... def inset_axes( self, @@ -268,6 +272,7 @@ class Axes(_AxesBase): self, xranges: Sequence[tuple[float, float]], yrange: tuple[float, float], + align: Literal["bottom", "center", "top"] = ..., *, data=..., **kwargs @@ -320,9 +325,18 @@ class Axes(_AxesBase): normalize: bool = ..., hatch: str | Sequence[str] | None = ..., data=..., - ) -> tuple[list[Wedge], list[Text]] | tuple[ - list[Wedge], list[Text], list[Text] - ]: ... + ) -> PieContainer: ... + def pie_label( + self, + container: PieContainer, + /, + labels: str | Sequence[str], + *, + distance: float = ..., + textprops: dict | None = ..., + rotate: bool = ..., + alignment: str = ..., + ) -> list[Text]: ... def errorbar( self, x: float | ArrayLike, diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index e5175ea8761c..ecff24540690 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -724,7 +724,6 @@ def __init__(self, fig, self.fmt_ydata = None self.set_navigate(True) - self.set_navigate_mode(None) if xscale: self.set_xscale(xscale) @@ -1737,6 +1736,8 @@ def get_adjustable(self): Return whether the Axes will adjust its physical dimension ('box') or its data limits ('datalim') to achieve the desired aspect ratio. + Newly created Axes default to 'box'. + See Also -------- matplotlib.axes.Axes.set_adjustable @@ -1762,6 +1763,8 @@ def set_adjustable(self, adjustable, share=False): See Also -------- + matplotlib.axes.Axes.get_adjustable + Return the current value of *adjustable*. matplotlib.axes.Axes.set_aspect For a description of aspect handling. @@ -1793,7 +1796,7 @@ def set_adjustable(self, adjustable, share=False): "Axes which override 'get_data_ratio'") for ax in axs: ax._adjustable = adjustable - self.stale = True + ax.stale = True def get_box_aspect(self): """ @@ -2337,6 +2340,23 @@ def add_child_axes(self, ax): def add_collection(self, collection, autolim=True): """ Add a `.Collection` to the Axes; return the collection. + + Parameters + ---------- + collection : `.Collection` + The collection to add. + autolim : bool + Whether to update data and view limits. + + .. versionchanged:: 3.11 + + This now also updates the view limits, making explicit + calls to `~.Axes.autoscale_view` unnecessary. + + As an implementation detail, the value "_datalim_only" is + supported to smooth the internal transition from pre-3.11 + behavior. This is not a public interface and will be removed + again in the future. """ _api.check_isinstance(mcoll.Collection, collection=collection) if not collection.get_label(): @@ -2361,7 +2381,19 @@ def add_collection(self, collection, autolim=True): # the call so that self.dataLim will update its own minpos. # This ensures that log scales see the correct minimum. points = np.concatenate([points, [datalim.minpos]]) - self.update_datalim(points) + # only update the dataLim for x/y if the collection uses transData + # in this direction. + x_is_data, y_is_data = (collection.get_transform() + .contains_branch_separately(self.transData)) + ox_is_data, oy_is_data = (collection.get_offset_transform() + .contains_branch_separately(self.transData)) + self.update_datalim( + points, + updatex=x_is_data or ox_is_data, + updatey=y_is_data or oy_is_data, + ) + if autolim != "_datalim_only": + self._request_autoscale_view() self.stale = True return collection @@ -2423,7 +2455,7 @@ def _update_line_limits(self, line): if line_trf == self.transData: data_path = path - elif any(line_trf.contains_branch_seperately(self.transData)): + elif any(line_trf.contains_branch_separately(self.transData)): # Compute the transform from line coordinates to data coordinates. trf_to_data = line_trf - self.transData # If transData is affine we can use the cached non-affine component @@ -2446,7 +2478,7 @@ def _update_line_limits(self, line): if not data_path.vertices.size: return - updatex, updatey = line_trf.contains_branch_seperately(self.transData) + updatex, updatey = line_trf.contains_branch_separately(self.transData) if self.name != "rectilinear": # This block is mostly intended to handle axvline in polar plots, # for which updatey would otherwise be True. @@ -2499,7 +2531,7 @@ def _update_patch_limits(self, patch): vertices = np.vstack(vertices) patch_trf = patch.get_transform() - updatex, updatey = patch_trf.contains_branch_seperately(self.transData) + updatex, updatey = patch_trf.contains_branch_separately(self.transData) if not (updatex or updatey): return if self.name != "rectilinear": @@ -3174,7 +3206,7 @@ def draw(self, renderer): if not self.get_figure(root=True).canvas.is_saving(): artists = [ a for a in artists - if not a.get_animated() or isinstance(a, mimage.AxesImage)] + if not a.get_animated()] artists = sorted(artists, key=attrgetter('zorder')) # rasterize artists with negative zorder @@ -4177,17 +4209,22 @@ def get_navigate_mode(self): """ Get the navigation toolbar button status: 'PAN', 'ZOOM', or None. """ - return self._navigate_mode + toolbar = self.figure.canvas.toolbar + if toolbar: + return None if toolbar.mode.name == "NONE" else toolbar.mode.name + manager = self.figure.canvas.manager + if manager and manager.toolmanager: + mode = manager.toolmanager.active_toggle.get("default") + return None if mode is None else mode.upper() + @_api.deprecated("3.11") def set_navigate_mode(self, b): """ Set the navigation toolbar button status. .. warning:: This is not a user-API function. - """ - self._navigate_mode = b def _get_view(self): """ @@ -4595,9 +4632,7 @@ def get_tightbbox(self, renderer=None, *, call_axes_locator=True, for a in bbox_artists: bbox = a.get_tightbbox(renderer) - if (bbox is not None - and 0 < bbox.width < np.inf - and 0 < bbox.height < np.inf): + if bbox is not None and bbox._is_finite(): bb.append(bbox) return mtransforms.Bbox.union( [b for b in bb if b.width != 0 or b.height != 0]) diff --git a/lib/matplotlib/axes/_base.pyi b/lib/matplotlib/axes/_base.pyi index 4933d0d1e236..5a5225eb8ba1 100644 --- a/lib/matplotlib/axes/_base.pyi +++ b/lib/matplotlib/axes/_base.pyi @@ -225,7 +225,7 @@ class _AxesBase(martist.Artist): ymin: float | None = ..., ymax: float | None = ... ) -> tuple[float, float, float, float]: ... - def get_legend(self) -> Legend: ... + def get_legend(self) -> Legend | None: ... def get_images(self) -> list[AxesImage]: ... def get_lines(self) -> list[Line2D]: ... def get_xaxis(self) -> XAxis: ... @@ -234,7 +234,7 @@ class _AxesBase(martist.Artist): def add_artist(self, a: Artist) -> Artist: ... def add_child_axes(self, ax: _AxesBase) -> _AxesBase: ... def add_collection( - self, collection: Collection, autolim: bool = ... + self, collection: Collection, autolim: bool | Literal["_datalim_only"] = ... ) -> Collection: ... def add_image(self, image: AxesImage) -> AxesImage: ... def add_line(self, line: Line2D) -> Line2D: ... diff --git a/lib/matplotlib/axes/_secondary_axes.py b/lib/matplotlib/axes/_secondary_axes.py index 15a1970fa4a6..08e706bba245 100644 --- a/lib/matplotlib/axes/_secondary_axes.py +++ b/lib/matplotlib/axes/_secondary_axes.py @@ -1,3 +1,4 @@ +import functools import numbers import numpy as np @@ -145,10 +146,25 @@ def apply_aspect(self, position=None): self._set_lims() super().apply_aspect(position) - @_docstring.copy(Axis.set_ticks) - def set_ticks(self, ticks, labels=None, *, minor=False, **kwargs): - ret = self._axis.set_ticks(ticks, labels, minor=minor, **kwargs) - self.stale = True + @functools.wraps(_AxesBase.set_xticks) + def set_xticks(self, *args, **kwargs): + if self._orientation == "y": + raise TypeError("Cannot set xticks on a secondary y-axis") + ret = super().set_xticks(*args, **kwargs) + self._ticks_set = True + return ret + + @functools.wraps(_AxesBase.set_yticks) + def set_yticks(self, *args, **kwargs): + if self._orientation == "x": + raise TypeError("Cannot set yticks on a secondary x-axis") + ret = super().set_yticks(*args, **kwargs) + self._ticks_set = True + return ret + + @functools.wraps(Axis.set_ticks) + def set_ticks(self, *args, **kwargs): + ret = self._axis.set_ticks(*args, **kwargs) self._ticks_set = True return ret diff --git a/lib/matplotlib/axes/_secondary_axes.pyi b/lib/matplotlib/axes/_secondary_axes.pyi index afb429f740c4..92bba590a4fa 100644 --- a/lib/matplotlib/axes/_secondary_axes.pyi +++ b/lib/matplotlib/axes/_secondary_axes.pyi @@ -29,6 +29,22 @@ class SecondaryAxis(_AxesBase): location: Literal["top", "bottom", "right", "left"] | float, transform: Transform | None = ... ) -> None: ... + def set_xticks( + self, + ticks: ArrayLike, + labels: Iterable[str] | None = ..., + *, + minor: bool = ..., + **kwargs + ) -> list[Tick]: ... + def set_yticks( + self, + ticks: ArrayLike, + labels: Iterable[str] | None = ..., + *, + minor: bool = ..., + **kwargs + ) -> list[Tick]: ... def set_ticks( self, ticks: ArrayLike, diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index fafdf92017f2..c3b6fcac569f 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1330,14 +1330,14 @@ def _update_ticks(self): return ticks_to_draw - def _get_ticklabel_bboxes(self, ticks, renderer=None): + def _get_ticklabel_bboxes(self, ticks, renderer): """Return lists of bboxes for ticks' label1's and label2's.""" - if renderer is None: - renderer = self.get_figure(root=True)._get_renderer() return ([tick.label1.get_window_extent(renderer) - for tick in ticks if tick.label1.get_visible()], + for tick in ticks + if tick.label1.get_visible() and tick.label1.get_in_layout()], [tick.label2.get_window_extent(renderer) - for tick in ticks if tick.label2.get_visible()]) + for tick in ticks + if tick.label2.get_visible() and tick.label2.get_in_layout()]) def get_tightbbox(self, renderer=None, *, for_layout_only=False): """ @@ -1383,8 +1383,7 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): bb.y0 = (bb.y0 + bb.y1) / 2 - 0.5 bb.y1 = bb.y0 + 1.0 bboxes.append(bb) - bboxes = [b for b in bboxes - if 0 < b.width < np.inf and 0 < b.height < np.inf] + bboxes = [b for b in bboxes if b._is_finite()] if bboxes: return mtransforms.Bbox.union(bboxes) else: diff --git a/lib/matplotlib/axis.pyi b/lib/matplotlib/axis.pyi index 6119b946fd7b..4bcfb1e1cfb7 100644 --- a/lib/matplotlib/axis.pyi +++ b/lib/matplotlib/axis.pyi @@ -1,7 +1,7 @@ from collections.abc import Callable, Iterable, Sequence import datetime from typing import Any, Literal, overload -from typing_extensions import Self # < Py 3.11 +from typing import Self import numpy as np from numpy.typing import ArrayLike diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 626852f2aa34..7f6c01089d0d 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -76,6 +76,7 @@ 'tif': 'Tagged Image File Format', 'tiff': 'Tagged Image File Format', 'webp': 'WebP Image Format', + 'avif': 'AV1 Image File Format', } _default_backends = { 'eps': 'matplotlib.backends.backend_ps', @@ -93,6 +94,7 @@ 'tif': 'matplotlib.backends.backend_agg', 'tiff': 'matplotlib.backends.backend_agg', 'webp': 'matplotlib.backends.backend_agg', + 'avif': 'matplotlib.backends.backend_agg', } @@ -1260,7 +1262,7 @@ class LocationEvent(Event): xdata, ydata : float or None Data coordinates of the mouse within *inaxes*, or *None* if the mouse is not over an Axes. - modifiers : frozenset + modifiers : frozenset[str] The keyboard modifiers currently being pressed (except for KeyEvent). """ @@ -1411,10 +1413,27 @@ def __init__(self, name, canvas, x, y, button=None, key=None, self.step = step self.dblclick = dblclick + @classmethod + def _from_ax_coords(cls, name, ax, xy, *args, **kwargs): + """ + Generate a synthetic event at a given axes coordinate. + + This method is intended for creating events during testing. The event + can be emitted by calling its ``_process()`` method. + + args and kwargs are mapped to `.MouseEvent.__init__` parameters, + starting with `button`. + """ + x, y = ax.transData.transform(xy) + event = cls(name, ax.figure.canvas, x, y, *args, **kwargs) + event.inaxes = ax + event.xdata, event.ydata = xy # Force exact xy to avoid fp roundtrip issues. + return event + def __str__(self): return (f"{self.name}: " f"xy=({self.x}, {self.y}) xydata=({self.xdata}, {self.ydata}) " - f"button={self.button} dblclick={self.dblclick} " + f"button={self.button} dblclick={self.dblclick} step={self.step} " f"inaxes={self.inaxes}") @@ -1503,6 +1522,22 @@ def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None): super().__init__(name, canvas, x, y, guiEvent=guiEvent) self.key = key + @classmethod + def _from_ax_coords(cls, name, ax, xy, key, *args, **kwargs): + """ + Generate a synthetic event at a given axes coordinate. + + This method is intended for creating events during testing. The event + can be emitted by calling its ``_process()`` method. + """ + # Separate from MouseEvent._from_ax_coords instead of being defined in the base + # class, due to different parameter order in the constructor signature. + x, y = ax.transData.transform(xy) + event = cls(name, ax.figure.canvas, key, x, y, *args, **kwargs) + event.inaxes = ax + event.xdata, event.ydata = xy # Force exact xy to avoid fp roundtrip issues. + return event + # Default callback for key events. def _key_handler(event): @@ -1705,6 +1740,10 @@ class FigureCanvasBase: filetypes = _default_filetypes + # global counter to assign unique ids to blit backgrounds + # see _get_blit_background_id() + _last_blit_background_id = 0 + @_api.classproperty def supports_blit(cls): """If this Canvas sub-class supports blitting.""" @@ -1728,8 +1767,9 @@ def __init__(self, figure=None): self.toolbar = None # NavigationToolbar2 will set me self._is_idle_drawing = False # We don't want to scale up the figure DPI more than once. - figure._original_dpi = figure.dpi + figure._original_dpi = getattr(figure, '_original_dpi', figure.dpi) self._device_pixel_ratio = 1 + self._blit_backgrounds = {} super().__init__() # Typically the GUI widget init (if any). callbacks = property(lambda self: self.figure._canvas_callbacks) @@ -1805,6 +1845,51 @@ def is_saving(self): def blit(self, bbox=None): """Blit the canvas in bbox (default entire canvas).""" + @classmethod + def _get_blit_background_id(cls): + """ + Get a globally unique id that can be used to store a blit background. + + Blitting support is canvas-dependent, so blitting mechanisms should + store their backgrounds in the canvas, more precisely in + ``canvas._blit_backgrounds[id]``. The id must be obtained via this + function to ensure it is globally unique. + + The content of ``canvas._blit_backgrounds[id]`` is not specified. + We leave this freedom to the blitting mechanism. + + Blitting mechanisms must not expect that a background that they + have stored is still there at a later time. The canvas may have + been switched out, or we may add other mechanisms later that + invalidate blit backgrounds (e.g. dpi changes). + Therefore, always query as `_blit_backgrounds.get(id)` and be + prepared for a None return value. + + Note: The blit background API is still experimental and may change + in the future without warning. + """ + cls._last_blit_background_id += 1 + return cls._last_blit_background_id + + def _release_blit_background_id(self, bb_id): + """ + Release a blit background id that is no longer needed. + + This removes the respective entry from the internal storage, i.e. + the ``canvas._blit_backgrounds`` dict, and thus allows to free the + associated memory. + + After releasing the id you must not use it anymore. + + It is safe to release an id that has not been used with the canvas + or that has already been released. + + Note: The blit background API is still experimental and may change + in the future without warning. + """ + if bb_id in self._blit_backgrounds: + del self._blit_backgrounds[bb_id] + def inaxes(self, xy): """ Return the topmost visible `~.axes.Axes` containing the point *xy*. @@ -2539,6 +2624,62 @@ def button_press_handler(event, canvas=None, toolbar=None): toolbar.forward() +def scroll_handler(event, canvas=None, toolbar=None): + ax = event.inaxes + if ax is None: + return + if ax.name != "rectilinear": + # zooming is currently only supported on rectilinear axes + return + + if toolbar is None: + toolbar = (canvas or event.canvas).toolbar + + if toolbar is None: + # technically we do not need a toolbar, but until wheel zoom was + # introduced, any interactive modification was only possible through + # the toolbar tools. For now, we keep the restriction that a toolbar + # is required for interactive navigation. + return + + if event.key in {"control", "x", "y"}: # zoom towards the mouse position + toolbar.push_current() + + xmin, xmax = ax.get_xlim() + ymin, ymax = ax.get_ylim() + (xmin, ymin), (xmax, ymax) = ax.transScale.transform( + [(xmin, ymin), (xmax, ymax)]) + + # mouse position in scaled (e.g., log) data coordinates + x, y = ax.transScale.transform((event.xdata, event.ydata)) + + scale_factor = 0.85 ** event.step + # Determine which axes to scale based on key + zoom_x = event.key in {"control", "x"} + zoom_y = event.key in {"control", "y"} + + if zoom_x: + new_xmin = x - (x - xmin) * scale_factor + new_xmax = x + (xmax - x) * scale_factor + else: + new_xmin, new_xmax = xmin, xmax + + if zoom_y: + new_ymin = y - (y - ymin) * scale_factor + new_ymax = y + (ymax - y) * scale_factor + else: + new_ymin, new_ymax = ymin, ymax + + inv_scale = ax.transScale.inverted() + (new_xmin, new_ymin), (new_xmax, new_ymax) = inv_scale.transform( + [(new_xmin, new_ymin), (new_xmax, new_ymax)]) + + ax.set_xlim(new_xmin, new_xmax) + ax.set_ylim(new_ymin, new_ymax) + + ax.figure.canvas.draw_idle() + + class NonGuiException(Exception): """Raised when trying show a figure in a non-GUI backend.""" pass @@ -2618,11 +2759,14 @@ def __init__(self, canvas, num): self.key_press_handler_id = None self.button_press_handler_id = None + self.scroll_handler_id = None if rcParams['toolbar'] != 'toolmanager': self.key_press_handler_id = self.canvas.mpl_connect( 'key_press_event', key_press_handler) self.button_press_handler_id = self.canvas.mpl_connect( 'button_press_event', button_press_handler) + self.scroll_handler_id = self.canvas.mpl_connect( + 'scroll_event', scroll_handler) self.toolmanager = (ToolManager(canvas.figure) if mpl.rcParams['toolbar'] == 'toolmanager' @@ -2723,7 +2867,9 @@ def show(self): f"shown") def destroy(self): - pass + # managers may have swapped the canvas to a GUI-framework specific one. + # restore the base canvas when the manager is destroyed. + self.canvas.figure._set_base_canvas() def full_screen_toggle(self): pass @@ -2763,10 +2909,6 @@ class _Mode(str, Enum): def __str__(self): return self.value - @property - def _navigate_mode(self): - return self.name if self is not _Mode.NONE else None - class NavigationToolbar2: """ @@ -3037,8 +3179,6 @@ def pan(self, *args): else: self.mode = _Mode.PAN self.canvas.widgetlock(self) - for a in self.canvas.figure.get_axes(): - a.set_navigate_mode(self.mode._navigate_mode) _PanInfo = namedtuple("_PanInfo", "button axes cid") @@ -3099,8 +3239,6 @@ def zoom(self, *args): else: self.mode = _Mode.ZOOM self.canvas.widgetlock(self) - for a in self.canvas.figure.get_axes(): - a.set_navigate_mode(self.mode._navigate_mode) _ZoomInfo = namedtuple("_ZoomInfo", "button start_xy axes cid cbar") diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index c65d39415472..a69b36093839 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -237,6 +237,7 @@ class LocationEvent(Event): inaxes: Axes | None xdata: float | None ydata: float | None + modifiers: frozenset[str] def __init__( self, name: str, @@ -407,6 +408,11 @@ def button_press_handler( canvas: FigureCanvasBase | None = ..., toolbar: NavigationToolbar2 | None = ..., ) -> None: ... +def scroll_handler( + event: MouseEvent, + canvas: FigureCanvasBase | None = ..., + toolbar: NavigationToolbar2 | None = ..., +) -> None: ... class NonGuiException(Exception): ... @@ -415,6 +421,7 @@ class FigureManagerBase: num: int | str key_press_handler_id: int | None button_press_handler_id: int | None + scroll_handler_id: int | None toolmanager: ToolManager | None toolbar: NavigationToolbar2 | ToolContainerBase | None def __init__(self, canvas: FigureCanvasBase, num: int | str) -> None: ... diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index 9410a73eff5f..0c03bd0800f4 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -668,9 +668,6 @@ def disable(self, event=None): def trigger(self, sender, event, data=None): self.toolmanager.get_tool(_views_positions).add_figure(self.figure) super().trigger(sender, event, data) - new_navigate_mode = self.name.upper() if self.toggled else None - for ax in self.figure.axes: - ax.set_navigate_mode(new_navigate_mode) def scroll_zoom(self, event): # https://gist.github.com/tacaswell/3144287 diff --git a/lib/matplotlib/backends/_backend_gtk.py b/lib/matplotlib/backends/_backend_gtk.py index ac443730e28a..ce6982a72526 100644 --- a/lib/matplotlib/backends/_backend_gtk.py +++ b/lib/matplotlib/backends/_backend_gtk.py @@ -195,6 +195,7 @@ def destroy(self, *args): self._destroying = True self.window.destroy() self.canvas.destroy() + super().destroy() @classmethod def start_main_loop(cls): diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index eaf868fd8bec..42782b2f00e1 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -22,8 +22,36 @@ TimerBase, ToolContainerBase, cursors, _Mode, MouseButton, CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) from matplotlib._pylab_helpers import Gcf -from . import _tkagg -from ._tkagg import TK_PHOTO_COMPOSITE_OVERLAY, TK_PHOTO_COMPOSITE_SET + +try: + from . import _tkagg + from ._tkagg import TK_PHOTO_COMPOSITE_OVERLAY, TK_PHOTO_COMPOSITE_SET +except ImportError as e: + # catch incompatibility of python-build-standalone with Tk + cause1 = getattr(e, '__cause__', None) + cause2 = getattr(cause1, '__cause__', None) + if (isinstance(cause1, ImportError) and + isinstance(cause2, AttributeError) and + "'_tkinter' has no attribute '__file__'" in str(cause2)): + + is_uv_python = "/uv/python" in (os.path.realpath(sys.executable)) + if is_uv_python: + raise ImportError( + "Failed to import tkagg backend. You appear to be using an outdated " + "version of uv's managed Python distribution which is not compatible " + "with Tk. Please upgrade to the latest uv version, then update " + "Python with: `uv python upgrade --reinstall`" + ) from e + else: + raise ImportError( + "Failed to import tkagg backend. This is likely caused by using a " + "Python executable based on python-build-standalone, which is not " + "compatible with Tk. Recent versions of python-build-standalone " + "should be compatible with Tk. Please update your python version " + "or select another backend." + ) from e + else: + raise _log = logging.getLogger(__name__) @@ -606,6 +634,7 @@ def delayed_destroy(): else: self.window.update() delayed_destroy() + super().destroy() def get_window_title(self): return self.window.wm_title() @@ -638,6 +667,14 @@ def __init__(self, canvas, window=None, *, pack_toolbar=True): window = canvas.get_tk_widget().master tk.Frame.__init__(self, master=window, borderwidth=2, width=int(canvas.figure.bbox.width), height=50) + # Avoid message_label expanding the toolbar size, and in turn expanding the + # canvas size. + # Without pack_propagate(False), when the user defines a small figure size + # (e.g. 2x2): + # 1. Figure size that is bigger than the user's expectation. + # 2. When message_label is refreshed by mouse enter/leave, the canvas + # size will also be changed. + self.pack_propagate(False) self._buttons = {} for text, tooltip_text, image_file, callback in self.toolitems: @@ -732,7 +769,7 @@ def draw_rubberband(self, event, x0, y0, x1, y1): y1 = height - y1 self.canvas._rubberband_rect_black = ( self.canvas._tkcanvas.create_rectangle( - x0, y0, x1, y1)) + x0, y0, x1, y1, outline='black')) self.canvas._rubberband_rect_white = ( self.canvas._tkcanvas.create_rectangle( x0, y0, x1, y1, outline='white', dash=(3, 3))) diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index b435ae565ce4..33b0be18ca2d 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -25,6 +25,7 @@ from math import radians, cos, sin import numpy as np +from PIL import features import matplotlib as mpl from matplotlib import _api, cbook @@ -510,7 +511,19 @@ def print_tif(self, filename_or_obj, *, metadata=None, pil_kwargs=None): def print_webp(self, filename_or_obj, *, metadata=None, pil_kwargs=None): self._print_pil(filename_or_obj, "webp", pil_kwargs, metadata) - print_gif.__doc__, print_jpg.__doc__, print_tif.__doc__, print_webp.__doc__ = map( + def print_avif(self, filename_or_obj, *, metadata=None, pil_kwargs=None): + if not features.check("avif"): + raise RuntimeError( + "The installed pillow version does not support avif. Full " + "avif support has been added in pillow 11.3." + ) + self._print_pil(filename_or_obj, "avif", pil_kwargs, metadata) + + (print_gif.__doc__, + print_jpg.__doc__, + print_tif.__doc__, + print_webp.__doc__, + print_avif.__doc__) = map( """ Write the figure to a {} file. @@ -518,10 +531,13 @@ def print_webp(self, filename_or_obj, *, metadata=None, pil_kwargs=None): ---------- filename_or_obj : str or path-like or file-like The file to write to. + metadata : None + Unused for pillow-based writers. All supported options + can be passed via *pil_kwargs*. pil_kwargs : dict, optional Additional keyword arguments that are passed to `PIL.Image.Image.save` when saving the figure. - """.format, ["GIF", "JPEG", "TIFF", "WebP"]) + """.format, ["GIF", "JPEG", "TIFF", "WebP", "AVIF"]) @_Backend.export diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 888f5a770f5d..20a1a3c8f0a9 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -10,14 +10,15 @@ CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) try: - import gi + from gi import require_version as gi_require_version except ImportError as err: raise ImportError("The GTK3 backends require PyGObject") from err try: # :raises ValueError: If module/version is already loaded, already # required, or unavailable. - gi.require_version("Gtk", "3.0") + gi_require_version("Gtk", "3.0") + gi_require_version("Gdk", "3.0") except ValueError as e: # in this case we want to re-raise as ImportError so the # auto-backend selection logic correctly skips. @@ -89,6 +90,7 @@ def __init__(self, figure=None): def destroy(self): CloseEvent("close_event", self)._process() + super().destroy() def set_cursor(self, cursor): # docstring inherited diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py index 620c9e5b94b6..95b116e9a6ba 100644 --- a/lib/matplotlib/backends/backend_gtk4.py +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -9,19 +9,22 @@ KeyEvent, LocationEvent, MouseEvent, ResizeEvent, CloseEvent) try: - import gi + from gi import require_version as gi_require_version except ImportError as err: raise ImportError("The GTK4 backends require PyGObject") from err try: # :raises ValueError: If module/version is already loaded, already # required, or unavailable. - gi.require_version("Gtk", "4.0") + gi_require_version("Gtk", "4.0") + gi_require_version("Gdk", "4.0") + gi_require_version("GdkPixbuf", "2.0") except ValueError as e: # in this case we want to re-raise as ImportError so the # auto-backend selection logic correctly skips. raise ImportError(e) from e +import gi from gi.repository import Gio, GLib, Gtk, Gdk, GdkPixbuf from . import _backend_gtk from ._backend_gtk import ( # noqa: F401 # pylint: disable=W0611 @@ -30,6 +33,7 @@ ) _GOBJECT_GE_3_47 = gi.version_info >= (3, 47, 0) +_GTK_GE_4_12 = Gtk.check_version(4, 12, 0) is None class FigureCanvasGTK4(_FigureCanvasGTK, Gtk.DrawingArea): @@ -48,7 +52,10 @@ def __init__(self, figure=None): self.set_draw_func(self._draw_func) self.connect('resize', self.resize_event) - self.connect('notify::scale-factor', self._update_device_pixel_ratio) + if _GTK_GE_4_12: + self.connect('realize', self._realize_event) + else: + self.connect('notify::scale-factor', self._update_device_pixel_ratio) click = Gtk.GestureClick() click.set_button(0) # All buttons. @@ -237,10 +244,20 @@ def _get_key(self, keyval, keycode, state): and not (mod == "shift" and unikey.isprintable()))] return "+".join([*mods, key]) + def _realize_event(self, obj): + surface = self.get_native().get_surface() + surface.connect('notify::scale', self._update_device_pixel_ratio) + self._update_device_pixel_ratio() + def _update_device_pixel_ratio(self, *args, **kwargs): # We need to be careful in cases with mixed resolution displays if # device_pixel_ratio changes. - if self._set_device_pixel_ratio(self.get_scale_factor()): + if _GTK_GE_4_12: + scale = self.get_native().get_surface().get_scale() + else: + scale = self.get_scale_factor() + assert scale is not None + if self._set_device_pixel_ratio(scale): self.draw() def _draw_rubberband(self, rect): diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index 4d18e1e9fb88..3ffec0910d79 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -142,6 +142,7 @@ def destroy(self): for comm in list(self.web_sockets): comm.on_close() self.clearup_closed() + super().destroy() def clearup_closed(self): """Clear up any closed Comms.""" diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index a75a8a86eb92..d63808eb3925 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -1035,11 +1035,10 @@ def _embedTeXFont(self, dvifont): fontdict['Encoding'] = self._generate_encoding(encoding) fc = fontdict['FirstChar'] = min(encoding.keys(), default=0) lc = fontdict['LastChar'] = max(encoding.keys(), default=255) - # Convert glyph widths from TeX 12.20 fixed point to 1/1000 text space units - tfm = dvifont._tfm - widths = [(1000 * metrics.tex_width) >> 20 - if (metrics := tfm.get_metrics(char)) else 0 + font_metrics = dvifont._metrics + widths = [(1000 * glyph_metrics.tex_width) >> 20 + if (glyph_metrics := font_metrics.get_metrics(char)) else 0 for char in range(fc, lc + 1)] fontdict['Widths'] = widthsObject = self.reserveObject('glyph widths') self.writeObject(widthsObject, widths) diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py index 9089e982cea6..0b0240c90310 100644 --- a/lib/matplotlib/backends/backend_qt.py +++ b/lib/matplotlib/backends/backend_qt.py @@ -14,7 +14,7 @@ import matplotlib.backends.qt_editor.figureoptions as figureoptions from . import qt_compat from .qt_compat import ( - QtCore, QtGui, QtWidgets, __version__, QT_API, _to_int, _isdeleted) + QtCore, QtGui, QtWidgets, QtSvg, __version__, QT_API, _to_int, _isdeleted) # SPECIAL_KEYS are Qt::Key that do *not* return their Unicode name @@ -55,7 +55,7 @@ ("Key_F8", "f8"), ("Key_F9", "f9"), ("Key_F10", "f10"), - ("Key_F10", "f11"), + ("Key_F11", "f11"), ("Key_F12", "f12"), ("Key_Super_L", "super"), ("Key_Super_R", "super"), @@ -262,12 +262,22 @@ def _update_screen(self, screen): screen.physicalDotsPerInchChanged.connect(self._update_pixel_ratio) screen.logicalDotsPerInchChanged.connect(self._update_pixel_ratio) + def eventFilter(self, source, event): + if event.type() == QtCore.QEvent.Type.DevicePixelRatioChange: + self._update_pixel_ratio() + return super().eventFilter(source, event) + def showEvent(self, event): # Set up correct pixel ratio, and connect to any signal changes for it, # once the window is shown (and thus has these attributes). window = self.window().windowHandle() - window.screenChanged.connect(self._update_screen) - self._update_screen(window.screen()) + current_version = tuple(int(x) for x in QtCore.qVersion().split('.', 2)[:2]) + if current_version >= (6, 6): + self._update_pixel_ratio() + window.installEventFilter(self) + else: + window.screenChanged.connect(self._update_screen) + self._update_screen(window.screen()) def set_cursor(self, cursor): # docstring inherited @@ -507,7 +517,7 @@ def _draw_idle(self): if not self._draw_pending: return self._draw_pending = False - if self.height() <= 0 or self.width() <= 0: + if _isdeleted(self) or self.height() <= 0 or self.width() <= 0: return try: self.draw() @@ -664,6 +674,7 @@ def destroy(self, *args): if self.toolbar: self.toolbar.destroy() self.window.close() + super().destroy() def get_window_title(self): return self.window.windowTitle() @@ -672,6 +683,120 @@ def set_window_title(self, title): self.window.setWindowTitle(title) +class _IconEngine(QtGui.QIconEngine): + """ + Custom QIconEngine that automatically handles DPI scaling for tools icons. + + This engine provides icons on-demand with proper scaling based on the current + device pixel ratio, eliminating the need for manual refresh when DPI changes. + """ + + def __init__(self, image_path, toolbar=None): + super().__init__() + self.image_path = image_path + self.toolbar = toolbar + + def _is_dark_mode(self): + return self.toolbar.palette().color(self.toolbar.backgroundRole()).value() < 128 + + def paint(self, painter, rect, mode, state): + """Paint the icon at the requested size and state.""" + pixmap = self.pixmap(rect.size(), mode, state) + if not pixmap.isNull(): + painter.drawPixmap(rect, pixmap) + + def pixmap(self, size, mode, state): + """Generate a pixmap for the requested size, mode, and state.""" + if size.width() <= 0 or size.height() <= 0: + return QtGui.QPixmap() + + # Try SVG first, then fall back to PNG + svg_path = self.image_path.with_suffix('.svg') + if svg_path.exists(): + pixmap = self._create_pixmap_from_svg(svg_path, size) + if not pixmap.isNull(): + return pixmap + return self._create_pixmap_from_png(self.image_path, size) + + def _devicePixelRatio(self): + """Return the current device pixel ratio for the toolbar, defaulting to 1.""" + return (self.toolbar.devicePixelRatioF() or 1) if self.toolbar else 1 + + def _create_pixmap_from_svg(self, svg_path, size): + """Create a pixmap from SVG with proper scaling and dark mode support.""" + QSvgRenderer = getattr(QtSvg, "QSvgRenderer", None) + if QSvgRenderer is None: + return QtGui.QPixmap() + + svg_content = svg_path.read_bytes() + + if self._is_dark_mode(): + svg_content = svg_content.replace(b'fill:black;', b'fill:white;') + svg_content = svg_content.replace(b'stroke:black;', b'stroke:white;') + + renderer = QSvgRenderer(QtCore.QByteArray(svg_content)) + if not renderer.isValid(): + return QtGui.QPixmap() + + dpr = self._devicePixelRatio() + scaled_size = QtCore.QSize(int(size.width() * dpr), int(size.height() * dpr)) + pixmap = QtGui.QPixmap(scaled_size) + pixmap.setDevicePixelRatio(dpr) + pixmap.fill(QtCore.Qt.GlobalColor.transparent) + + painter = QtGui.QPainter() + try: + painter.begin(pixmap) + renderer.render(painter, QtCore.QRectF(0, 0, size.width(), size.height())) + finally: + if painter.isActive(): + painter.end() + + return pixmap + + def _create_pixmap_from_png(self, base_path, size): + """ + Create a pixmap from PNG with scaling and dark mode support. + + Prefer to use the *_large.png with the same name; otherwise, use base_path. + """ + large_path = base_path.with_name(base_path.stem + '_large.png') + source_pixmap = QtGui.QPixmap() + for candidate in (large_path, base_path): + if not candidate.exists(): + continue + candidate_pixmap = QtGui.QPixmap(str(candidate)) + if not candidate_pixmap.isNull(): + source_pixmap = candidate_pixmap + break + if source_pixmap.isNull(): + return source_pixmap + + dpr = self._devicePixelRatio() + + # Scale to requested size + scaled_size = QtCore.QSize(int(size.width() * dpr), int(size.height() * dpr)) + scaled_pixmap = source_pixmap.scaled( + scaled_size, + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation + ) + scaled_pixmap.setDevicePixelRatio(dpr) + + if self._is_dark_mode(): + # On some platforms (e.g., macOS with Qt5 in dark mode), this may + # incorrectly return a black color instead of a light one. + # See issue #27590 for details. + icon_color = self.toolbar.palette().color(self.toolbar.foregroundRole()) + mask = scaled_pixmap.createMaskFromColor( + QtGui.QColor('black'), + QtCore.Qt.MaskMode.MaskOutColor) + scaled_pixmap.fill(icon_color) + scaled_pixmap.setMask(mask) + + return scaled_pixmap + + class NavigationToolbar2QT(NavigationToolbar2, QtWidgets.QToolBar): toolitems = [*NavigationToolbar2.toolitems] toolitems.insert( @@ -729,25 +854,16 @@ def _icon(self, name): """ Construct a `.QIcon` from an image file *name*, including the extension and relative to Matplotlib's "images" data directory. + + Uses _IconEngine for automatic DPI scaling. """ - # use a high-resolution icon with suffix '_large' if available - # note: user-provided icons may not have '_large' versions + # Get the image path path_regular = cbook._get_data_path('images', name) - path_large = path_regular.with_name( - path_regular.name.replace('.png', '_large.png')) - filename = str(path_large if path_large.exists() else path_regular) - - pm = QtGui.QPixmap(filename) - pm.setDevicePixelRatio( - self.devicePixelRatioF() or 1) # rarely, devicePixelRatioF=0 - if self.palette().color(self.backgroundRole()).value() < 128: - icon_color = self.palette().color(self.foregroundRole()) - mask = pm.createMaskFromColor( - QtGui.QColor('black'), - QtCore.Qt.MaskMode.MaskOutColor) - pm.fill(icon_color) - pm.setMask(mask) - return QtGui.QIcon(pm) + + # Create icon using our custom engine for automatic DPI handling + engine = _IconEngine(path_regular, self) + return QtGui.QIcon(engine) + def edit_parameters(self): axes = self.canvas.figure.get_axes() diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 0cb6430ec823..c1f824007a56 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1134,7 +1134,8 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): font_style['font-style'] = prop.get_style() if prop.get_variant() != 'normal': font_style['font-variant'] = prop.get_variant() - weight = fm.weight_dict[prop.get_weight()] + weight = prop.get_weight() + weight = fm.weight_dict.get(weight, weight) # convert to int if weight != 400: font_style['font-weight'] = f'{weight}' diff --git a/lib/matplotlib/backends/backend_webagg.py b/lib/matplotlib/backends/backend_webagg.py index dfc5747ef77c..e4808e8d0d32 100644 --- a/lib/matplotlib/backends/backend_webagg.py +++ b/lib/matplotlib/backends/backend_webagg.py @@ -21,14 +21,12 @@ import threading try: - import tornado + import tornado.web + import tornado.ioloop + import tornado.websocket except ImportError as err: raise RuntimeError("The WebAgg backend requires Tornado.") from err -import tornado.web -import tornado.ioloop -import tornado.websocket - import matplotlib as mpl from matplotlib.backend_bases import _Backend from matplotlib._pylab_helpers import Gcf diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index 33490ff66dc5..4afe088db8d1 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -178,6 +178,9 @@ def __init__(self, *args, **kwargs): # Track mouse events to fill in the x, y position of key events. self._last_mouse_xy = (None, None) + # Control whether scroll events prevent default browser behavior + self._capture_scroll = False + def show(self): # show the figure window from matplotlib.pyplot import show @@ -224,6 +227,28 @@ def set_image_mode(self, mode): self._current_image_mode = mode self.handle_send_image_mode(None) + def set_capture_scroll(self, capture): + """ + Set whether the scroll events on the canvas will scroll the page. + + Parameters + ---------- + capture : bool + """ + if self._capture_scroll != capture: + self._capture_scroll = capture + self.send_event("capture_scroll", capture_scroll=capture) + + def get_capture_scroll(self): + """ + Get whether scroll events are currently captured by the canvas. + + Returns + ------- + bool + """ + return self._capture_scroll + def get_diff_image(self): if self._png_is_old: renderer = self.get_renderer() @@ -335,6 +360,8 @@ def handle_refresh(self, event): # Normal toolbar init would refresh this, but it happens before the # browser canvas is set up. self.toolbar.set_history_buttons() + # Send the current capture_scroll state to newly connected clients + self.send_event('capture_scroll', capture_scroll=self._capture_scroll) self.draw_idle() def handle_resize(self, event): diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index f83a69d8361e..0acb4499ed87 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -30,7 +30,7 @@ from matplotlib.transforms import Affine2D import wx -import wx.svg +import wx.svg # noqa: F401 _log = logging.getLogger(__name__) @@ -1012,6 +1012,7 @@ def destroy(self, *args): # As this can be called from non-GUI thread from plt.close use # wx.CallAfter to ensure thread safety. wx.CallAfter(frame.Close) + super().destroy() def full_screen_toggle(self): # docstring inherited diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index b57a98b1138a..8f666c734b06 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -64,11 +64,11 @@ def _setup_pyqt5plus(): - global QtCore, QtGui, QtWidgets, __version__ + global QtCore, QtGui, QtWidgets, QtSvg, __version__ global _isdeleted, _to_int if QT_API == QT_API_PYQT6: - from PyQt6 import QtCore, QtGui, QtWidgets, sip + from PyQt6 import QtCore, QtGui, QtWidgets, sip, QtSvg __version__ = QtCore.PYQT_VERSION_STR QtCore.Signal = QtCore.pyqtSignal QtCore.Slot = QtCore.pyqtSlot @@ -76,7 +76,7 @@ def _setup_pyqt5plus(): _isdeleted = sip.isdeleted _to_int = operator.attrgetter('value') elif QT_API == QT_API_PYSIDE6: - from PySide6 import QtCore, QtGui, QtWidgets, __version__ + from PySide6 import QtCore, QtGui, QtWidgets, QtSvg, __version__ import shiboken6 def _isdeleted(obj): return not shiboken6.isValid(obj) if parse_version(__version__) >= parse_version('6.4'): @@ -84,7 +84,7 @@ def _isdeleted(obj): return not shiboken6.isValid(obj) else: _to_int = int elif QT_API == QT_API_PYQT5: - from PyQt5 import QtCore, QtGui, QtWidgets + from PyQt5 import QtCore, QtGui, QtWidgets, QtSvg import sip __version__ = QtCore.PYQT_VERSION_STR QtCore.Signal = QtCore.pyqtSignal @@ -93,7 +93,7 @@ def _isdeleted(obj): return not shiboken6.isValid(obj) _isdeleted = sip.isdeleted _to_int = int elif QT_API == QT_API_PYSIDE2: - from PySide2 import QtCore, QtGui, QtWidgets, __version__ + from PySide2 import QtCore, QtGui, QtWidgets, QtSvg, __version__ try: from PySide2 import shiboken2 except ImportError: diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index 9d31fa9ced2c..cd4e9583cce5 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -92,7 +92,7 @@ def prepare_data(d, init): `"None"`, `"none"` and `""` are synonyms); *init* is one shorthand of the initial style. - This function returns an list suitable for initializing a + This function returns a list suitable for initializing a FormLayout combobox, namely `[initial_name, (shorthand, style_name), (shorthand, style_name), ...]`. """ @@ -194,7 +194,7 @@ def apply_callback(data): raise ValueError("Unexpected field") title = general.pop(0) - axes.set_title(title) + axes.title.set_text(title) generate_legend = general.pop() for i, (name, axis) in enumerate(axis_map.items()): diff --git a/lib/matplotlib/backends/web_backend/js/mpl.js b/lib/matplotlib/backends/web_backend/js/mpl.js index 303260773a2f..7745cbcf1e98 100644 --- a/lib/matplotlib/backends/web_backend/js/mpl.js +++ b/lib/matplotlib/backends/web_backend/js/mpl.js @@ -24,6 +24,8 @@ mpl.figure = function (figure_id, websocket, ondownload, parent_element) { this.supports_binary = this.ws.binaryType !== undefined; + this.capture_scroll = false; + if (!this.supports_binary) { var warnings = document.getElementById('mpl-warnings'); if (warnings) { @@ -313,6 +315,9 @@ mpl.figure.prototype._init_canvas = function () { } else { event.step = -1; } + if (fig.capture_scroll) { + event.preventDefault(); + } on_mouse_event_closure('scroll')(event); }); @@ -320,7 +325,6 @@ mpl.figure.prototype._init_canvas = function () { canvas_div.appendChild(rubberband_canvas); this.rubberband_context = rubberband_canvas.getContext('2d'); - this.rubberband_context.strokeStyle = '#000000'; this._resize_canvas = function (width, height, forward) { if (forward) { @@ -464,19 +468,38 @@ mpl.figure.prototype.handle_rubberband = function (fig, msg) { y0 = Math.floor(y0) + 0.5; x1 = Math.floor(x1) + 0.5; y1 = Math.floor(y1) + 0.5; - var min_x = Math.min(x0, x1); - var min_y = Math.min(y0, y1); - var width = Math.abs(x1 - x0); - var height = Math.abs(y1 - y0); - fig.rubberband_context.clearRect( + var ctx = fig.rubberband_context; + ctx.clearRect( 0, 0, fig.canvas.width / fig.ratio, fig.canvas.height / fig.ratio ); - fig.rubberband_context.strokeRect(min_x, min_y, width, height); + var drawRubberband = function () { + // Draw the lines from x0, y0 towards x1, y1 so that the + // dashes don't "jump" when moving the zoom box. + ctx.beginPath(); + ctx.moveTo(x0, y0); + ctx.lineTo(x0, y1); + ctx.moveTo(x0, y0); + ctx.lineTo(x1, y0); + ctx.moveTo(x0, y1); + ctx.lineTo(x1, y1); + ctx.moveTo(x1, y0); + ctx.lineTo(x1, y1); + ctx.stroke(); + }; + + fig.rubberband_context.lineWidth = 1; + fig.rubberband_context.setLineDash([3]); + fig.rubberband_context.lineDashOffset = 0; + fig.rubberband_context.strokeStyle = '#000000'; + drawRubberband(); + fig.rubberband_context.strokeStyle = '#ffffff'; + fig.rubberband_context.lineDashOffset = 3; + drawRubberband(); }; mpl.figure.prototype.handle_figure_label = function (fig, msg) { @@ -524,6 +547,10 @@ mpl.figure.prototype.handle_navigate_mode = function (fig, msg) { } }; +mpl.figure.prototype.handle_capture_scroll = function (fig, msg) { + fig.capture_scroll = msg['capture_scroll']; +}; + mpl.figure.prototype.updated_canvas_event = function () { // Called whenever the canvas gets updated. this.send_message('ack', {}); diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index a09780965b0c..2e416486baf4 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -29,7 +29,7 @@ from numpy import VisibleDeprecationWarning import matplotlib -from matplotlib import _api, _c_internal_utils +from matplotlib import _api, _c_internal_utils, mlab class _ExceptionInfo: @@ -43,16 +43,20 @@ class _ExceptionInfo: users and result in incorrect tracebacks. """ - def __init__(self, cls, *args): + def __init__(self, cls, *args, notes=None): self._cls = cls self._args = args + self._notes = notes if notes is not None else [] @classmethod def from_exception(cls, exc): - return cls(type(exc), *exc.args) + return cls(type(exc), *exc.args, notes=getattr(exc, "__notes__", [])) def to_exception(self): - return self._cls(*self._args) + exc = self._cls(*self._args) + for note in self._notes: + exc.add_note(note) + return exc def _get_running_interactive_framework(): @@ -690,7 +694,21 @@ def safe_masked_invalid(x, copy=False): try: xm = np.ma.masked_where(~(np.isfinite(x)), x, copy=False) except TypeError: - return x + if len(x.dtype.descr) == 1: + # Arrays with dtype 'object' get returned here. + # For example the 'c' kwarg of scatter, which supports multiple types. + # `plt.scatter([3, 4], [2, 5], c=[(1, 0, 0), 'y'])` + return x + else: + # In case of a dtype with multiple fields + # for example image data using a MultiNorm + try: + mask = np.empty(x.shape, dtype=np.dtype('bool, '*len(x.dtype.descr))) + for dd, dm in zip(x.dtype.descr, mask.dtype.descr): + mask[dm[0]] = ~np.isfinite(x[dd[0]]) + xm = np.ma.array(x, mask=mask, copy=False) + except TypeError: + return x return xm @@ -1430,7 +1448,7 @@ def _reshape_2D(X, name): return result -def violin_stats(X, method, points=100, quantiles=None): +def violin_stats(X, method=("GaussianKDE", "scott"), points=100, quantiles=None): """ Return a list of dictionaries of data which can be used to draw a series of violin plots. @@ -1439,21 +1457,40 @@ def violin_stats(X, method, points=100, quantiles=None): dictionary. Users can skip this function and pass a user-defined set of dictionaries - with the same keys to `~.axes.Axes.violinplot` instead of using Matplotlib + with the same keys to `~.axes.Axes.violin` instead of using Matplotlib to do the calculations. See the *Returns* section below for the keys that must be present in the dictionaries. Parameters ---------- - X : array-like + X : 1D array or sequence of 1D arrays or 2D array Sample data that will be used to produce the gaussian kernel density - estimates. Must have 2 or fewer dimensions. + estimates. Possible values: + + - 1D array: Statistics are computed for that array. + - sequence of 1D arrays: Statistics are computed for each array in the sequence. + - 2D array: Statistics are computed for each column in the array. - method : callable + method : (name, bw_method) or callable, The method used to calculate the kernel density estimate for each - column of data. When called via ``method(v, coords)``, it should - return a vector of the values of the KDE evaluated at the values - specified in coords. + column of data. Valid values: + + - a tuple of the form ``(name, bw_method)`` where *name* currently must + always be ``"GaussianKDE"`` and *bw_method* is the method used to + calculate the estimator bandwidth. Supported values are 'scott', + 'silverman' or a float or a callable. If a float, this will be used + directly as `!kde.factor`. If a callable, it should take a + `matplotlib.mlab.GaussianKDE` instance as its only parameter and + return a float. + + - a callable with the signature :: + + def method(data: ndarray, coords: ndarray) -> ndarray + + It should return the KDE of *data* evaluated at *coords*. + + .. versionadded:: 3.11 + Support for ``(name, bw_method)`` tuple. points : int, default: 100 Defines the number of points to evaluate each of the gaussian kernel @@ -1481,6 +1518,20 @@ def violin_stats(X, method, points=100, quantiles=None): - max: The maximum value for this column of data. - quantiles: The quantile values for this column of data. """ + if isinstance(method, tuple): + name, bw_method = method + if name != "GaussianKDE": + raise ValueError(f"Unknown KDE method name {name!r}. The only supported " + 'named method is "GaussianKDE"') + + def _kde_method(x, coords): + # fallback gracefully if the vector contains only one value + if np.all(x[0] == x): + return (x[0] == coords).astype(float) + kde = mlab.GaussianKDE(x, bw_method) + return kde.evaluate(coords) + + method = _kde_method # List of dictionaries describing each of the violins. vpstats = [] diff --git a/lib/matplotlib/cbook.pyi b/lib/matplotlib/cbook.pyi index 6c2d9c303eb2..f7959a6fd0bb 100644 --- a/lib/matplotlib/cbook.pyi +++ b/lib/matplotlib/cbook.pyi @@ -14,10 +14,10 @@ from typing import ( Generic, IO, Literal, - Sequence, TypeVar, overload, ) +from collections.abc import Sequence _T = TypeVar("_T") @@ -133,7 +133,10 @@ ls_mapper_r: dict[str, str] def contiguous_regions(mask: ArrayLike) -> list[np.ndarray]: ... def is_math_text(s: str) -> bool: ... def violin_stats( - X: ArrayLike, method: Callable, points: int = ..., quantiles: ArrayLike | None = ... + X: ArrayLike, + method: tuple[Literal["GaussianKDE"], Literal["scott", "silverman"] | float | Callable] | Callable = ..., + points: int = ..., + quantiles: ArrayLike | None = ... ) -> list[dict[str, Any]]: ... def pts_to_prestep(x: ArrayLike, *args: ArrayLike) -> np.ndarray: ... def pts_to_poststep(x: ArrayLike, *args: ArrayLike) -> np.ndarray: ... diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index ef5bf0719d3b..497f0c2debdf 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -92,10 +92,8 @@ def __init__(self, cmaps): self._builtin_cmaps = tuple(cmaps) def __getitem__(self, item): - try: - return self._cmaps[item].copy() - except KeyError: - raise KeyError(f"{item!r} is not a known colormap name") from None + cmap = _api.check_getitem(self._cmaps, colormap=item, _error_cls=KeyError) + return cmap.copy() def __iter__(self): return iter(self._cmaps) @@ -241,32 +239,3 @@ def get_cmap(self, cmap): _multivar_colormaps = ColormapRegistry(multivar_cmaps) _bivar_colormaps = ColormapRegistry(bivar_cmaps) - - -def _ensure_cmap(cmap): - """ - Ensure that we have a `.Colormap` object. - - For internal use to preserve type stability of errors. - - Parameters - ---------- - cmap : None, str, Colormap - - - if a `Colormap`, return it - - if a string, look it up in mpl.colormaps - - if None, look up the default color map in mpl.colormaps - - Returns - ------- - Colormap - - """ - if isinstance(cmap, colors.Colormap): - return cmap - cmap_name = mpl._val_or_rc(cmap, "image.cmap") - # use check_in_list to ensure type stability of the exception raised by - # the internal usage of this (ValueError vs KeyError) - if cmap_name not in _colormaps: - _api.check_in_list(sorted(_colormaps), cmap=cmap_name) - return mpl.colormaps[cmap_name] diff --git a/lib/matplotlib/cm.pyi b/lib/matplotlib/cm.pyi index 366b336fe04d..f4b9fb9ea8dd 100644 --- a/lib/matplotlib/cm.pyi +++ b/lib/matplotlib/cm.pyi @@ -20,3 +20,187 @@ _multivar_colormaps: ColormapRegistry = ... _bivar_colormaps: ColormapRegistry = ... ScalarMappable = _ScalarMappable + +magma: colors.Colormap +inferno: colors.Colormap +plasma: colors.Colormap +viridis: colors.Colormap +cividis: colors.Colormap +twilight: colors.Colormap +twilight_shifted: colors.Colormap +turbo: colors.Colormap +berlin: colors.Colormap +managua: colors.Colormap +vanimo: colors.Colormap +Blues: colors.Colormap +BrBG: colors.Colormap +BuGn: colors.Colormap +BuPu: colors.Colormap +CMRmap: colors.Colormap +GnBu: colors.Colormap +Greens: colors.Colormap +Greys: colors.Colormap +OrRd: colors.Colormap +Oranges: colors.Colormap +PRGn: colors.Colormap +PiYG: colors.Colormap +PuBu: colors.Colormap +PuBuGn: colors.Colormap +PuOr: colors.Colormap +PuRd: colors.Colormap +Purples: colors.Colormap +RdBu: colors.Colormap +RdGy: colors.Colormap +RdPu: colors.Colormap +RdYlBu: colors.Colormap +RdYlGn: colors.Colormap +Reds: colors.Colormap +Spectral: colors.Colormap +Wistia: colors.Colormap +YlGn: colors.Colormap +YlGnBu: colors.Colormap +YlOrBr: colors.Colormap +YlOrRd: colors.Colormap +afmhot: colors.Colormap +autumn: colors.Colormap +binary: colors.Colormap +bone: colors.Colormap +brg: colors.Colormap +bwr: colors.Colormap +cool: colors.Colormap +coolwarm: colors.Colormap +copper: colors.Colormap +cubehelix: colors.Colormap +flag: colors.Colormap +gist_earth: colors.Colormap +gist_gray: colors.Colormap +gist_heat: colors.Colormap +gist_ncar: colors.Colormap +gist_rainbow: colors.Colormap +gist_stern: colors.Colormap +gist_yarg: colors.Colormap +gnuplot: colors.Colormap +gnuplot2: colors.Colormap +gray: colors.Colormap +hot: colors.Colormap +hsv: colors.Colormap +jet: colors.Colormap +nipy_spectral: colors.Colormap +ocean: colors.Colormap +pink: colors.Colormap +prism: colors.Colormap +rainbow: colors.Colormap +seismic: colors.Colormap +spring: colors.Colormap +summer: colors.Colormap +terrain: colors.Colormap +winter: colors.Colormap +Accent: colors.Colormap +Dark2: colors.Colormap +okabe_ito: colors.Colormap +Paired: colors.Colormap +Pastel1: colors.Colormap +Pastel2: colors.Colormap +Set1: colors.Colormap +Set2: colors.Colormap +Set3: colors.Colormap +tab10: colors.Colormap +tab20: colors.Colormap +tab20b: colors.Colormap +tab20c: colors.Colormap +grey: colors.Colormap +gist_grey: colors.Colormap +gist_yerg: colors.Colormap +Grays: colors.Colormap +# Reversed colormaps +magma_r: colors.Colormap +inferno_r: colors.Colormap +plasma_r: colors.Colormap +viridis_r: colors.Colormap +cividis_r: colors.Colormap +twilight_r: colors.Colormap +twilight_shifted_r: colors.Colormap +turbo_r: colors.Colormap +berlin_r: colors.Colormap +managua_r: colors.Colormap +vanimo_r: colors.Colormap +Blues_r: colors.Colormap +BrBG_r: colors.Colormap +BuGn_r: colors.Colormap +BuPu_r: colors.Colormap +CMRmap_r: colors.Colormap +GnBu_r: colors.Colormap +Greens_r: colors.Colormap +Greys_r: colors.Colormap +OrRd_r: colors.Colormap +Oranges_r: colors.Colormap +PRGn_r: colors.Colormap +PiYG_r: colors.Colormap +PuBu_r: colors.Colormap +PuBuGn_r: colors.Colormap +PuOr_r: colors.Colormap +PuRd_r: colors.Colormap +Purples_r: colors.Colormap +RdBu_r: colors.Colormap +RdGy_r: colors.Colormap +RdPu_r: colors.Colormap +RdYlBu_r: colors.Colormap +RdYlGn_r: colors.Colormap +Reds_r: colors.Colormap +Spectral_r: colors.Colormap +Wistia_r: colors.Colormap +YlGn_r: colors.Colormap +YlGnBu_r: colors.Colormap +YlOrBr_r: colors.Colormap +YlOrRd_r: colors.Colormap +afmhot_r: colors.Colormap +autumn_r: colors.Colormap +binary_r: colors.Colormap +bone_r: colors.Colormap +brg_r: colors.Colormap +bwr_r: colors.Colormap +cool_r: colors.Colormap +coolwarm_r: colors.Colormap +copper_r: colors.Colormap +cubehelix_r: colors.Colormap +flag_r: colors.Colormap +gist_earth_r: colors.Colormap +gist_gray_r: colors.Colormap +gist_heat_r: colors.Colormap +gist_ncar_r: colors.Colormap +gist_rainbow_r: colors.Colormap +gist_stern_r: colors.Colormap +gist_yarg_r: colors.Colormap +gnuplot_r: colors.Colormap +gnuplot2_r: colors.Colormap +gray_r: colors.Colormap +hot_r: colors.Colormap +hsv_r: colors.Colormap +jet_r: colors.Colormap +nipy_spectral_r: colors.Colormap +ocean_r: colors.Colormap +pink_r: colors.Colormap +prism_r: colors.Colormap +rainbow_r: colors.Colormap +seismic_r: colors.Colormap +spring_r: colors.Colormap +summer_r: colors.Colormap +terrain_r: colors.Colormap +winter_r: colors.Colormap +Accent_r: colors.Colormap +Dark2_r: colors.Colormap +okabe_ito_r: colors.Colormap +Paired_r: colors.Colormap +Pastel1_r: colors.Colormap +Pastel2_r: colors.Colormap +Set1_r: colors.Colormap +Set2_r: colors.Colormap +Set3_r: colors.Colormap +tab10_r: colors.Colormap +tab20_r: colors.Colormap +tab20b_r: colors.Colormap +tab20c_r: colors.Colormap +grey_r: colors.Colormap +gist_grey_r: colors.Colormap +gist_yerg_r: colors.Colormap +Grays_r: colors.Colormap diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index ec6d40805da0..ceae9fc308a0 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -283,7 +283,7 @@ def get_datalim(self, transData): offsets = self.get_offsets() - if any(transform.contains_branch_seperately(transData)): + if any(transform.contains_branch_separately(transData)): # collections that are just in data units (like quiver) # can properly have the axes limits set by their shape + # offset. LineCollections that have no offsets can @@ -670,27 +670,43 @@ def set_linestyle(self, ls): """ Set the linestyle(s) for the collection. - =========================== ================= - linestyle description - =========================== ================= - ``'-'`` or ``'solid'`` solid line - ``'--'`` or ``'dashed'`` dashed line - ``'-.'`` or ``'dashdot'`` dash-dotted line - ``':'`` or ``'dotted'`` dotted line - =========================== ================= + Parameters + ---------- + ls : {'-', '--', '-.', ':', '', ...} or (offset, on-off-seq) or list thereof + If a list, the individual elements are assigned to the elements of the + collection. - Alternatively a dash tuple of the following form can be provided:: + Possible values: - (offset, onoffseq), + - A string: - where ``onoffseq`` is an even length tuple of on and off ink in points. + ======================================================= ================ + linestyle description + ======================================================= ================ + ``'-'`` or ``'solid'`` solid line + ``'--'`` or ``'dashed'`` dashed line + ``'-.'`` or ``'dashdot'`` dash-dotted line + ``':'`` or ``'dotted'`` dotted line + ``''`` or ``'none'`` (discouraged: ``'None'``, ``' '``) draw nothing + ======================================================= ================ - Parameters - ---------- - ls : str or tuple or list thereof - Valid values for individual linestyles include {'-', '--', '-.', - ':', '', (offset, on-off-seq)}. See `.Line2D.set_linestyle` for a - complete description. + - A tuple describing the start position and lengths of dashes and spaces: + + (offset, onoffseq) + + where + + - *offset* is a float specifying the offset (in points); i.e. how much + is the dash pattern shifted. + - *onoffseq* is a sequence of on and off ink in points. There can be + arbitrary many pairs of on and off values. + + Example: The tuple ``(0, (10, 5, 1, 5))`` means that the pattern starts + at the beginning of the line. It draws a 10 point long dash, + then a 5 point long space, then a 1 point long dash, followed by a 5 point + long space, and then the pattern repeats. + + For examples see :doc:`/gallery/lines_bars_and_markers/linestyles`. """ # get the list of raw 'unscaled' dash patterns self._us_linestyles = mlines._get_dash_patterns(ls) diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 4348f02cfc34..a4292a323035 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -373,7 +373,7 @@ def __init__( colors=[mpl.rcParams['axes.edgecolor']], linewidths=[0.5 * mpl.rcParams['axes.linewidth']], clip_on=False) - self.ax.add_collection(self.dividers) + self.ax.add_collection(self.dividers, autolim=False) self._locator = None self._minorlocator = None @@ -807,7 +807,7 @@ def add_lines(self, *args, **kwargs): xy = self.ax.transAxes.inverted().transform(inches.transform(xy)) col.set_clip_path(mpath.Path(xy, closed=True), self.ax.transAxes) - self.ax.add_collection(col) + self.ax.add_collection(col, autolim=False) self.stale = True def update_ticks(self): @@ -1098,7 +1098,7 @@ def _process_values(self): # If we still aren't scaled after autoscaling, use 0, 1 as default self.norm.vmin = 0 self.norm.vmax = 1 - self.norm.vmin, self.norm.vmax = mtransforms.nonsingular( + self.norm.vmin, self.norm.vmax = mtransforms._nonsingular( self.norm.vmin, self.norm.vmax, expander=0.1) if (not isinstance(self.norm, colors.BoundaryNorm) and (self.boundaries is None)): diff --git a/lib/matplotlib/colorizer.py b/lib/matplotlib/colorizer.py index 92a6e4ea4c4f..120e816fed45 100644 --- a/lib/matplotlib/colorizer.py +++ b/lib/matplotlib/colorizer.py @@ -24,7 +24,7 @@ import numpy as np from numpy import ma -from matplotlib import _api, colors, cbook, scale, artist +from matplotlib import _api, colors, cbook, artist, scale import matplotlib as mpl mpl._docstring.interpd.register( @@ -78,7 +78,7 @@ def _scale_norm(self, norm, vmin, vmax, A): raise ValueError( "Passing a Normalize instance simultaneously with " "vmin/vmax is not supported. Please pass vmin/vmax " - "directly to the norm when creating it.") + "as arguments to the norm object when creating it") # always resolve the autoscaling so we have concrete limits # rather than deferring to draw time. @@ -90,19 +90,7 @@ def norm(self): @norm.setter def norm(self, norm): - _api.check_isinstance((colors.Norm, str, None), norm=norm) - if norm is None: - norm = colors.Normalize() - elif isinstance(norm, str): - try: - scale_cls = scale._scale_mapping[norm] - except KeyError: - raise ValueError( - "Invalid norm str name; the following values are " - f"supported: {', '.join(scale._scale_mapping)}" - ) from None - norm = _auto_norm_from_scale(scale_cls)() - + norm = _ensure_norm(norm, n_components=self.cmap.n_variates) if norm is self.norm: # We aren't updating anything return @@ -186,7 +174,7 @@ def _pass_image_data(x, alpha=None, bytes=False, norm=True): if norm and (xx.max() > 1 or xx.min() < 0): raise ValueError("Floating point image RGB values " - "must be in the 0..1 range.") + "must be in the [0,1] range") if bytes: xx = (xx * 255).astype(np.uint8) elif xx.dtype == np.uint8: @@ -231,10 +219,13 @@ def _set_cmap(self, cmap): ---------- cmap : `.Colormap` or str or None """ - # bury import to avoid circular imports - from matplotlib import cm in_init = self._cmap is None - self._cmap = cm._ensure_cmap(cmap) + cmap_obj = _ensure_cmap(cmap, accept_multivariate=True) + if not in_init and self.norm.n_components != cmap_obj.n_variates: + raise ValueError(f"The colormap {cmap} does not support " + f"{self.norm.n_components} variates as required by " + f"the {type(self.norm)} on this Colorizer") + self._cmap = cmap_obj if not in_init: self.changed() # Things are not set up properly yet. @@ -255,29 +246,29 @@ def set_clim(self, vmin=None, vmax=None): vmin, vmax : float The limits. - The limits may also be passed as a tuple (*vmin*, *vmax*) as a - single positional argument. + For scalar data, the limits may also be passed as a + tuple (*vmin*, *vmax*) single positional argument. .. ACCEPTS: (vmin: float, vmax: float) """ - # If the norm's limits are updated self.changed() will be called - # through the callbacks attached to the norm, this causes an inconsistent - # state, to prevent this blocked context manager is used - if vmax is None: - try: - vmin, vmax = vmin - except (TypeError, ValueError): - pass + if self.norm.n_components == 1: + if vmax is None: + try: + vmin, vmax = vmin + except (TypeError, ValueError): + pass orig_vmin_vmax = self.norm.vmin, self.norm.vmax # Blocked context manager prevents callbacks from being triggered # until both vmin and vmax are updated with self.norm.callbacks.blocked(signal='changed'): + # Since the @vmin/vmax.setter invokes colors._sanitize_extrema() + # to sanitize the input, the input is not sanitized here if vmin is not None: - self.norm.vmin = colors._sanitize_extrema(vmin) + self.norm.vmin = vmin if vmax is not None: - self.norm.vmax = colors._sanitize_extrema(vmax) + self.norm.vmax = vmax # emit a update signal if the limits are changed if orig_vmin_vmax != (self.norm.vmin, self.norm.vmax): @@ -476,31 +467,51 @@ def _format_cursor_data_override(self, data): # Note if cm.ScalarMappable is depreciated, this functionality should be # implemented as format_cursor_data() on ColorizingArtist. - n = self.cmap.N - if np.ma.getmask(data): + if np.ma.getmask(data) or data is None: + # NOTE: for multivariate data, if *any* of the fields are masked, + # "[]" is returned here return "[]" - normed = self.norm(data) + + if isinstance(self.norm, colors.MultiNorm): + norms = self.norm.norms + if isinstance(self.cmap, colors.BivarColormap): + n_s = (self.cmap.N, self.cmap.M) + else: # colors.MultivarColormap + n_s = [part.N for part in self.cmap] + else: # colors.Colormap + norms = [self.norm] + data = [data] + n_s = [self.cmap.N] + + os = [f"{d:-#.{self._sig_digits_from_norm(no, d, n)}g}" + for no, d, n in zip(norms, data, n_s)] + return f"[{', '.join(os)}]" + + @staticmethod + def _sig_digits_from_norm(norm, data, n): + # Determines the number of significant digits + # to use for a number given a norm, and n, where n is the + # number of colors in the colormap. + normed = norm(data) if np.isfinite(normed): - if isinstance(self.norm, colors.BoundaryNorm): + if isinstance(norm, colors.BoundaryNorm): # not an invertible normalization mapping - cur_idx = np.argmin(np.abs(self.norm.boundaries - data)) + cur_idx = np.argmin(np.abs(norm.boundaries - data)) neigh_idx = max(0, cur_idx - 1) # use max diff to prevent delta == 0 - delta = np.diff( - self.norm.boundaries[neigh_idx:cur_idx + 2] - ).max() - elif self.norm.vmin == self.norm.vmax: + delta = np.diff(norm.boundaries[neigh_idx:cur_idx + 2]).max() + elif norm.vmin == norm.vmax: # singular norms, use delta of 10% of only value - delta = np.abs(self.norm.vmin * .1) + delta = np.abs(norm.vmin * .1) else: # Midpoints of neighboring color intervals. - neighbors = self.norm.inverse( - (int(normed * n) + np.array([0, 1])) / n) + neighbors = norm.inverse((int(normed * n) + np.array([0, 1])) / n) delta = abs(neighbors - data).max() + g_sig_digits = cbook._g_sig_digits(data, delta) else: g_sig_digits = 3 # Consistent with default below. - return f"[{data:-#.{g_sig_digits}g}]" + return g_sig_digits class _ScalarMappable(_ColorizerInterface): @@ -563,11 +574,19 @@ def set_array(self, A): self._A = None return + A = _ensure_multivariate_data(A, self.norm.n_components) + A = cbook.safe_masked_invalid(A, copy=True) if not np.can_cast(A.dtype, float, "same_kind"): - raise TypeError(f"Image data of dtype {A.dtype} cannot be " - "converted to float") + if A.dtype.fields is None: + raise TypeError(f"Image data of dtype {A.dtype} cannot be " + f"converted to float") + else: + for key in A.dtype.fields: + if not np.can_cast(A[key].dtype, float, "same_kind"): + raise TypeError(f"Image data of dtype {A.dtype} cannot be " + f"converted to a sequence of floats") self._A = A if not self.norm.scaled(): self._colorizer.autoscale_None(A) @@ -615,6 +634,15 @@ def _get_colorizer(cmap, norm, colorizer): cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` The Colormap instance or registered colormap name used to map scalar data to colors.""", + multi_cmap_doc="""\ +cmap : str, `~matplotlib.colors.Colormap`, `~matplotlib.colors.BivarColormap`\ + or `~matplotlib.colors.MultivarColormap`, default: :rc:`image.cmap` + The Colormap instance or registered colormap name used to map + data values to colors. + + Multivariate data is only accepted if a multivariate colormap + (`~matplotlib.colors.BivarColormap` or `~matplotlib.colors.MultivarColormap`) + is used.""", norm_doc="""\ norm : str or `~matplotlib.colors.Normalize`, optional The normalization method used to scale scalar data to the [0, 1] range @@ -629,6 +657,21 @@ def _get_colorizer(cmap, norm, colorizer): list of available scales, call `matplotlib.scale.get_scale_names()`. In that case, a suitable `.Normalize` subclass is dynamically generated and instantiated.""", + multi_norm_doc="""\ +norm : str, `~matplotlib.colors.Normalize` or list, optional + The normalization method used to scale data to the [0, 1] range + before mapping to colors using *cmap*. By default, a linear scaling is + used, mapping the lowest value to 0 and the highest to 1. + This can be one of the following: + - An instance of `.Normalize` or one of its subclasses + (see :ref:`colormapnorms`). + - A scale name, i.e. one of "linear", "log", "symlog", "logit", etc. For a + list of available scales, call `matplotlib.scale.get_scale_names()`. + In this case, a suitable `.Normalize` subclass is dynamically generated + and instantiated. + - A list of scale names or `.Normalize` objects matching the number of + variates in the colormap, for use with `~matplotlib.colors.BivarColormap` + or `~matplotlib.colors.MultivarColormap`, i.e. ``["linear", "log"]``.""", vmin_vmax_doc="""\ vmin, vmax : float, optional When using scalar data and no explicit *norm*, *vmin* and *vmax* define @@ -636,6 +679,17 @@ def _get_colorizer(cmap, norm, colorizer): the complete value range of the supplied data. It is an error to use *vmin*/*vmax* when a *norm* instance is given (but using a `str` *norm* name together with *vmin*/*vmax* is acceptable).""", + multi_vmin_vmax_doc="""\ +vmin, vmax : float or list, optional + When using scalar data and no explicit *norm*, *vmin* and *vmax* define + the data range that the colormap covers. By default, the colormap covers + the complete value range of the supplied data. It is an error to use + *vmin*/*vmax* when a *norm* instance is given (but using a `str` *norm* + name together with *vmin*/*vmax* is acceptable). + + A list of values (vmin or vmax) can be used to define independent limits + for each variate when using a `~matplotlib.colors.BivarColormap` or + `~matplotlib.colors.MultivarColormap`.""", ) @@ -701,3 +755,155 @@ def _auto_norm_from_scale(scale_cls): norm = colors.make_norm_from_scale(scale_cls)( colors.Normalize)() return type(norm) + + +def _ensure_norm(norm, n_components=1): + if n_components == 1: + _api.check_isinstance((colors.Norm, str, None), norm=norm) + if norm is None: + norm = colors.Normalize() + elif isinstance(norm, str): + scale_cls = _api.check_getitem(scale._scale_mapping, norm=norm) + return _auto_norm_from_scale(scale_cls)() + return norm + elif n_components > 1: + if not np.iterable(norm): + _api.check_isinstance((colors.MultiNorm, None, tuple), norm=norm) + if norm is None: + norm = colors.MultiNorm(['linear']*n_components) + else: # iterable, i.e. multiple strings or Normalize objects + norm = colors.MultiNorm(norm) + if isinstance(norm, colors.MultiNorm) and norm.n_components == n_components: + return norm + raise ValueError( + f"Invalid norm for multivariate colormap with {n_components} inputs") + else: # n_components == 0 + raise ValueError( + "Invalid cmap. A colorizer object must have a cmap with `n_variates` >= 1") + + +def _ensure_cmap(cmap, accept_multivariate=False): + """ + Ensure that we have a `.Colormap` object. + + For internal use to preserve type stability of errors. + + Parameters + ---------- + cmap : None, str, Colormap + + - if a `~matplotlib.colors.Colormap`, + `~matplotlib.colors.MultivarColormap` or + `~matplotlib.colors.BivarColormap`, + return it + - if a string, look it up in three corresponding databases + when not found: raise an error based on the expected shape + - if None, look up the default color map in mpl.colormaps + accept_multivariate : bool, default False + - if False, accept only Colormap, string in mpl.colormaps or None + + Returns + ------- + Colormap + + """ + if accept_multivariate: + types = (colors.Colormap, colors.BivarColormap, colors.MultivarColormap) + mappings = (mpl.colormaps, mpl.multivar_colormaps, mpl.bivar_colormaps) + else: + types = (colors.Colormap, ) + mappings = (mpl.colormaps, ) + + if isinstance(cmap, types): + return cmap + + cmap_name = mpl._val_or_rc(cmap, "image.cmap") + + for mapping in mappings: + if cmap_name in mapping: + return mapping[cmap_name] + + # this error message is a variant of _api.check_in_list but gives + # additional hints as to how to access multivariate colormaps + + raise ValueError(f"{cmap!r} is not a valid value for cmap" + "; supported values for scalar colormaps are " + f"{', '.join(map(repr, sorted(mpl.colormaps)))}\n" + "See `matplotlib.bivar_colormaps()` and" + " `matplotlib.multivar_colormaps()` for" + " bivariate and multivariate colormaps") + + +def _ensure_multivariate_data(data, n_components): + """ + Ensure that the data has dtype with n_components. + Input data of shape (n_components, n, m) is converted to an array of shape + (n, m) with data type np.dtype(f'{data.dtype}, ' * n_components) + Complex data is returned as a view with dtype np.dtype('float64, float64') + or np.dtype('float32, float32') + If n_components is 1 and data is not of type np.ndarray (i.e. PIL.Image), + the data is returned unchanged. + If data is None, the function returns None + + Parameters + ---------- + n_components : int + Number of variates in the data. + data : np.ndarray, PIL.Image or None + + Returns + ------- + np.ndarray, PIL.Image or None + """ + + if isinstance(data, np.ndarray): + if len(data.dtype.descr) == n_components: + # pass scalar data + # and already formatted data + return data + elif data.dtype in [np.complex64, np.complex128]: + if n_components != 2: + raise ValueError("Invalid data entry for multivariate data. " + "Complex numbers are incompatible with " + f"{n_components} variates.") + + # pass complex data + if data.dtype == np.complex128: + dt = np.dtype('float64, float64') + else: + dt = np.dtype('float32, float32') + + reconstructed = np.ma.array(np.ma.getdata(data).view(dt)) + if np.ma.is_masked(data): + for descriptor in dt.descr: + reconstructed[descriptor[0]][data.mask] = np.ma.masked + return reconstructed + + if n_components > 1 and len(data) == n_components: + # convert data from shape (n_components, n, m) + # to (n, m) with a new dtype + data = [np.ma.array(part, copy=False) for part in data] + dt = np.dtype(', '.join([f'{part.dtype}' for part in data])) + fields = [descriptor[0] for descriptor in dt.descr] + reconstructed = np.ma.empty(data[0].shape, dtype=dt) + for i, f in enumerate(fields): + if data[i].shape != reconstructed.shape: + raise ValueError("For multivariate data all variates must have same " + f"shape, not {data[0].shape} and {data[i].shape}") + reconstructed[f] = data[i] + if np.ma.is_masked(data[i]): + reconstructed[f][data[i].mask] = np.ma.masked + return reconstructed + + if n_components == 1: + # PIL.Image gets passed here + return data + + elif n_components == 2: + raise ValueError("Invalid data entry for multivariate data. The data" + " must contain complex numbers, or have a first dimension 2," + " or be of a dtype with 2 fields") + else: + raise ValueError("Invalid data entry for multivariate data. The shape" + f" of the data must have a first dimension {n_components}" + f" or be of a dtype with {n_components} fields") diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index a09b4f3d4f5c..c5dd5d3b13fe 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -55,7 +55,7 @@ import matplotlib as mpl import numpy as np -from matplotlib import _api, _cm, cbook, scale, _image +from matplotlib import _api, _cm, cbook, scale from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS @@ -128,6 +128,7 @@ class ColorSequenceRegistry(Mapping): 'Pastel2': _cm._Pastel2_data, 'Paired': _cm._Paired_data, 'Accent': _cm._Accent_data, + 'okabe_ito': _cm._okabe_ito_data, 'Dark2': _cm._Dark2_data, 'Set1': _cm._Set1_data, 'Set2': _cm._Set2_data, @@ -805,8 +806,7 @@ def _get_rgba_and_mask(self, X, alpha=None, bytes=False): mask : np.ndarray Boolean array with True where the input is ``np.nan`` or masked. """ - if not self._isinit: - self._init() + self._ensure_inited() xa = np.array(X, copy=True) if not xa.dtype.isnative: @@ -863,59 +863,60 @@ def __eq__(self, other): self.colorbar_extend != other.colorbar_extend): return False # To compare lookup tables the Colormaps have to be initialized - if not self._isinit: - self._init() - if not other._isinit: - other._init() + self._ensure_inited() + other._ensure_inited() return np.array_equal(self._lut, other._lut) def get_bad(self): """Get the color for masked values.""" - if not self._isinit: - self._init() + self._ensure_inited() return np.array(self._lut[self._i_bad]) + @_api.deprecated( + "3.11", + pending=True, + alternative="cmap.with_extremes(bad=...) or Colormap(bad=...)") def set_bad(self, color='k', alpha=None): """Set the color for masked values.""" - self._rgba_bad = to_rgba(color, alpha) - if self._isinit: - self._set_extremes() + self._set_extremes(bad=(color, alpha)) def get_under(self): """Get the color for low out-of-range values.""" - if not self._isinit: - self._init() + self._ensure_inited() return np.array(self._lut[self._i_under]) + @_api.deprecated( + "3.11", + pending=True, + alternative="cmap.with_extremes(under=...) or Colormap(under=...)") def set_under(self, color='k', alpha=None): """Set the color for low out-of-range values.""" - self._rgba_under = to_rgba(color, alpha) - if self._isinit: - self._set_extremes() + self._set_extremes(under=(color, alpha)) def get_over(self): """Get the color for high out-of-range values.""" - if not self._isinit: - self._init() + self._ensure_inited() return np.array(self._lut[self._i_over]) + @_api.deprecated( + "3.11", + pending=True, + alternative="cmap.with_extremes(over=...) or Colormap(over=...)") def set_over(self, color='k', alpha=None): """Set the color for high out-of-range values.""" - self._rgba_over = to_rgba(color, alpha) - if self._isinit: - self._set_extremes() + self._set_extremes(over=(color, alpha)) + @_api.deprecated( + "3.11", + pending=True, + alternative="cmap.with_extremes(bad=..., under=..., over=...) or " + "Colormap(bad=..., under=..., over=...)") def set_extremes(self, *, bad=None, under=None, over=None): """ Set the colors for masked (*bad*) values and, when ``norm.clip = False``, low (*under*) and high (*over*) out-of-range values. """ - if bad is not None: - self.set_bad(bad) - if under is not None: - self.set_under(under) - if over is not None: - self.set_over(over) + self._set_extremes(bad=bad, under=under, over=over) def with_extremes(self, *, bad=None, under=None, over=None): """ @@ -924,10 +925,26 @@ def with_extremes(self, *, bad=None, under=None, over=None): out-of-range values, have been set accordingly. """ new_cm = self.copy() - new_cm.set_extremes(bad=bad, under=under, over=over) + new_cm._set_extremes(bad=bad, under=under, over=over) return new_cm - def _set_extremes(self): + def _set_extremes(self, bad=None, under=None, over=None): + """ + Set the colors for masked (*bad*) and out-of-range (*under* and *over*) values. + + Parameters that are None are left unchanged. + """ + if bad is not None: + self._rgba_bad = to_rgba(bad) + if under is not None: + self._rgba_under = to_rgba(under) + if over is not None: + self._rgba_over = to_rgba(over) + if self._isinit: + self._update_lut_extremes() + + def _update_lut_extremes(self): + """Ensure than an existing lookup table has the correct extreme values.""" if self._rgba_under: self._lut[self._i_under] = self._rgba_under else: @@ -952,8 +969,7 @@ def with_alpha(self, alpha): if not 0 <= alpha <= 1: raise ValueError("'alpha' must be between 0 and 1, inclusive") new_cm = self.copy() - if not new_cm._isinit: - new_cm._init() + new_cm._ensure_inited() new_cm._lut[:, 3] = alpha return new_cm @@ -961,10 +977,13 @@ def _init(self): """Generate the lookup table, ``self._lut``.""" raise NotImplementedError("Abstract class only") - def is_gray(self): - """Return whether the colormap is grayscale.""" + def _ensure_inited(self): if not self._isinit: self._init() + + def is_gray(self): + """Return whether the colormap is grayscale.""" + self._ensure_inited() return (np.all(self._lut[:, 0] == self._lut[:, 1]) and np.all(self._lut[:, 0] == self._lut[:, 2])) @@ -1154,7 +1173,7 @@ def _init(self): self._lut[:-3, 3] = _create_lookup_table( self.N, self._segmentdata['alpha'], 1) self._isinit = True - self._set_extremes() + self._update_lut_extremes() def set_gamma(self, gamma): """Set a new gamma value and regenerate colormap.""" @@ -1322,7 +1341,7 @@ class ListedColormap(Colormap): "and will be removed in %(removal)s. Please ensure the list " "of passed colors is the required length instead." ) - def __init__(self, colors, name='from_list', N=None, *, + def __init__(self, colors, name='unnamed', N=None, *, bad=None, under=None, over=None): if N is None: self.colors = colors @@ -1346,7 +1365,7 @@ def _init(self): self._lut = np.zeros((self.N + 3, 4), float) self._lut[:-3] = to_rgba_array(self.colors) self._isinit = True - self._set_extremes() + self._update_lut_extremes() @property def monochrome(self): @@ -1358,9 +1377,7 @@ def monochrome(self): # TODO: It's a separate discussion whether we need this property on # colormaps at all (at least as public API). It's a very special edge # case and we only use it for contours internally. - if not self._isinit: - self._init() - + self._ensure_inited() return self.N <= 1 or np.all(self._lut[0] == self._lut[1:self.N]) def resampled(self, lutsize): @@ -1615,14 +1632,16 @@ def with_extremes(self, *, bad=None, under=None, over=None): f" i.e. be of length {len(new_cm)}.") else: for c, b in zip(new_cm, under): - c.set_under(b) + # in-place change is ok, since we've just created c as a copy + c._set_extremes(under=b) if over is not None: if not np.iterable(over) or len(over) != len(new_cm): raise ValueError("*over* must contain a color for each scalar colormap" f" i.e. be of length {len(new_cm)}.") else: for c, b in zip(new_cm, over): - c.set_over(b) + # in-place change is ok, since we've just created c as a copy + c._set_extremes(over=b) return new_cm @property @@ -2071,26 +2090,27 @@ def __getitem__(self, item): """Creates and returns a colorbar along the selected axis""" if not self._isinit: self._init() + extremes = ( + dict(bad=self._rgba_bad, over=self._rgba_outside, under=self._rgba_outside) + if self.shape in ['ignore', 'circleignore'] + else dict(bad=self._rgba_bad) + ) if item == 0: origin_1_as_int = int(self._origin[1]*self.M) if origin_1_as_int > self.M-1: origin_1_as_int = self.M-1 one_d_lut = self._lut[:, origin_1_as_int] - new_cmap = ListedColormap(one_d_lut, name=f'{self.name}_0') + new_cmap = ListedColormap(one_d_lut, name=f'{self.name}_0', **extremes) elif item == 1: origin_0_as_int = int(self._origin[0]*self.N) if origin_0_as_int > self.N-1: origin_0_as_int = self.N-1 one_d_lut = self._lut[origin_0_as_int, :] - new_cmap = ListedColormap(one_d_lut, name=f'{self.name}_1') + new_cmap = ListedColormap(one_d_lut, name=f'{self.name}_1', **extremes) else: raise KeyError(f"only 0 or 1 are" f" valid keys for BivarColormap, not {item!r}") - new_cmap._rgba_bad = self._rgba_bad - if self.shape in ['ignore', 'circleignore']: - new_cmap.set_over(self._rgba_outside) - new_cmap.set_under(self._rgba_outside) return new_cmap def _repr_png_(self): @@ -2192,16 +2212,37 @@ def __init__(self, patch, N=256, shape='square', origin=(0, 0), super().__init__(N, N, shape, origin, name=name) def _init(self): + # Perform bilinear interpolation + s = self.patch.shape - _patch = np.empty((s[0], s[1], 4)) - _patch[:, :, :3] = self.patch - _patch[:, :, 3] = 1 - transform = mpl.transforms.Affine2D().translate(-0.5, -0.5)\ - .scale(self.N / (s[1] - 1), self.N / (s[0] - 1)) - self._lut = np.empty((self.N, self.N, 4)) - - _image.resample(_patch, self._lut, transform, _image.BILINEAR, - resample=False, alpha=1) + + # Indices (whole and fraction) of the new grid points + row = np.linspace(0, s[0] - 1, self.N)[:, np.newaxis] + col = np.linspace(0, s[1] - 1, self.N)[np.newaxis, :] + left = row.astype(int) # floor not needed because all values are nonnegative + top = col.astype(int) # floor not needed because all values are nonnegative + row_frac = (row - left)[:, :, np.newaxis] + col_frac = (col - top)[:, :, np.newaxis] + + # Indices of the next edges, clipping where needed + right = np.clip(left + 1, 0, s[0] - 1) + bottom = np.clip(top + 1, 0, s[1] - 1) + + # Values at the corners + tl = self.patch[left, top, :] + tr = self.patch[right, top, :] + bl = self.patch[left, bottom, :] + br = self.patch[right, bottom, :] + + # Interpolate between the corners + lut = (tl * (1 - row_frac) * (1 - col_frac) + + tr * row_frac * (1 - col_frac) + + bl * (1 - row_frac) * col_frac + + br * row_frac * col_frac) + + # Add the alpha channel + self._lut = np.concatenate([lut, np.ones((self.N, self.N, 1))], axis=2) + self._isinit = True @@ -2337,6 +2378,17 @@ def _changed(self): """ self.callbacks.process('changed') + @property + @abstractmethod + def n_components(self): + """ + The number of normalized components. + + This is the number of elements of the parameter to ``__call__`` and of + *vmin*, *vmax*. + """ + pass + class Normalize(Norm): """ @@ -2547,6 +2599,19 @@ def scaled(self): # docstring inherited return self.vmin is not None and self.vmax is not None + @property + def n_components(self): + """ + The number of distinct components supported (1). + + This is the number of elements of the parameter to ``__call__`` and of + *vmin*, *vmax*. + + This class support only a single component, as opposed to `MultiNorm` + which supports multiple components. + """ + return 1 + class TwoSlopeNorm(Normalize): def __init__(self, vcenter, vmin=None, vmax=None): @@ -3272,6 +3337,300 @@ def inverse(self, value): return value +class MultiNorm(Norm): + """ + A class which contains multiple scalar norms. + """ + + def __init__(self, norms, vmin=None, vmax=None, clip=None): + """ + Parameters + ---------- + norms : list of (str or `Normalize`) + The constituent norms. The list must have a minimum length of 1. + vmin, vmax : None or list of (float or None) + Limits of the constituent norms. + If a list, one value is assigned to each of the constituent + norms. + If None, the limits of the constituent norms + are not changed. + clip : None or list of bools, default: None + Determines the behavior for mapping values outside the range + ``[vmin, vmax]`` for the constituent norms. + If a list, each value is assigned to each of the constituent + norms. + If None, the behaviour of the constituent norms is not changed. + """ + if cbook.is_scalar_or_string(norms): + raise ValueError( + "MultiNorm must be assigned an iterable of norms, where each " + f"norm is of type `str`, or `Normalize`, not {type(norms)}") + + if len(norms) < 1: + raise ValueError("MultiNorm must be assigned at least one norm") + + def resolve(norm): + if isinstance(norm, str): + scale_cls = _api.check_getitem(scale._scale_mapping, norm=norm) + return mpl.colorizer._auto_norm_from_scale(scale_cls)() + elif isinstance(norm, Normalize): + return norm + else: + raise ValueError( + "Each norm assigned to MultiNorm must be " + f"of type `str`, or `Normalize`, not {type(norm)}") + + self._norms = tuple(resolve(norm) for norm in norms) + + self.callbacks = cbook.CallbackRegistry(signals=["changed"]) + + self.vmin = vmin + self.vmax = vmax + self.clip = clip + + for n in self._norms: + n.callbacks.connect('changed', self._changed) + + @property + def n_components(self): + """Number of norms held by this `MultiNorm`.""" + return len(self._norms) + + @property + def norms(self): + """The individual norms held by this `MultiNorm`.""" + return self._norms + + @property + def vmin(self): + """The lower limit of each constituent norm.""" + return tuple(n.vmin for n in self._norms) + + @vmin.setter + def vmin(self, values): + if values is None: + return + if not np.iterable(values) or len(values) != self.n_components: + raise ValueError("*vmin* must have one component for each norm. " + f"Expected an iterable of length {self.n_components}, " + f"but got {values!r}") + with self.callbacks.blocked(): + for norm, v in zip(self.norms, values): + norm.vmin = v + self._changed() + + @property + def vmax(self): + """The upper limit of each constituent norm.""" + return tuple(n.vmax for n in self._norms) + + @vmax.setter + def vmax(self, values): + if values is None: + return + if not np.iterable(values) or len(values) != self.n_components: + raise ValueError("*vmax* must have one component for each norm. " + f"Expected an iterable of length {self.n_components}, " + f"but got {values!r}") + with self.callbacks.blocked(): + for norm, v in zip(self.norms, values): + norm.vmax = v + self._changed() + + @property + def clip(self): + """The clip behaviour of each constituent norm.""" + return tuple(n.clip for n in self._norms) + + @clip.setter + def clip(self, values): + if values is None: + return + if not np.iterable(values) or len(values) != self.n_components: + raise ValueError("*clip* must have one component for each norm. " + f"Expected an iterable of length {self.n_components}, " + f"but got {values!r}") + with self.callbacks.blocked(): + for norm, v in zip(self.norms, values): + norm.clip = v + self._changed() + + def _changed(self): + """ + Call this whenever the norm is changed to notify all the + callback listeners to the 'changed' signal. + """ + self.callbacks.process('changed') + + def __call__(self, values, clip=None): + """ + Normalize the data and return the normalized data. + + Each component of the input is normalized via the constituent norm. + + Parameters + ---------- + values : array-like + The input data, as an iterable or a structured numpy array. + + - If iterable, must be of length `n_components`. Each element can be a + scalar or array-like and is normalized through the corresponding norm. + - If structured array, must have `n_components` fields. Each field + is normalized through the corresponding norm. + + clip : list of bools or None, optional + Determines the behavior for mapping values outside the range + ``[vmin, vmax]``. See the description of the parameter *clip* in + `.Normalize`. + If ``None``, defaults to ``self.clip`` (which defaults to + ``False``). + + Returns + ------- + tuple + Normalized input values + + Notes + ----- + If not already initialized, ``self.vmin`` and ``self.vmax`` are + initialized using ``self.autoscale_None(values)``. + """ + if clip is None: + clip = self.clip + if not np.iterable(clip) or len(clip) != self.n_components: + raise ValueError("*clip* must have one component for each norm. " + f"Expected an iterable of length {self.n_components}, " + f"but got {clip!r}") + + values = self._iterable_components_in_data(values, self.n_components) + result = tuple(n(v, clip=c) for n, v, c in zip(self.norms, values, clip)) + return result + + def inverse(self, values): + """ + Map the normalized values (i.e., index in the colormap) back to data values. + + Parameters + ---------- + values : array-like + The input data, as an iterable or a structured numpy array. + + - If iterable, must be of length `n_components`. Each element can be a + scalar or array-like and is mapped through the corresponding norm. + - If structured array, must have `n_components` fields. Each field + is mapped through the the corresponding norm. + + """ + values = self._iterable_components_in_data(values, self.n_components) + result = tuple(n.inverse(v) for n, v in zip(self.norms, values)) + return result + + def autoscale(self, A): + """ + For each constituent norm, set *vmin*, *vmax* to min, max of the corresponding + component in *A*. + + Parameters + ---------- + A : array-like + The input data, as an iterable or a structured numpy array. + + - If iterable, must be of length `n_components`. Each element + is used for the limits of one constituent norm. + - If structured array, must have `n_components` fields. Each field + is used for the limits of one constituent norm. + """ + with self.callbacks.blocked(): + A = self._iterable_components_in_data(A, self.n_components) + for n, a in zip(self.norms, A): + n.autoscale(a) + self._changed() + + def autoscale_None(self, A): + """ + If *vmin* or *vmax* are not set on any constituent norm, + use the min/max of the corresponding component in *A* to set them. + + Parameters + ---------- + A : array-like + The input data, as an iterable or a structured numpy array. + + - If iterable, must be of length `n_components`. Each element + is used for the limits of one constituent norm. + - If structured array, must have `n_components` fields. Each field + is used for the limits of one constituent norm. + """ + with self.callbacks.blocked(): + A = self._iterable_components_in_data(A, self.n_components) + for n, a in zip(self.norms, A): + n.autoscale_None(a) + self._changed() + + def scaled(self): + """Return whether both *vmin* and *vmax* are set on all constituent norms.""" + return all(n.scaled() for n in self.norms) + + @staticmethod + def _iterable_components_in_data(data, n_components): + """ + Provides an iterable over the components contained in the data. + + An input array with `n_components` fields is returned as a tuple of length n + referencing slices of the original array. + + Parameters + ---------- + data : array-like + The input data, as an iterable or a structured numpy array. + + - If iterable, must be of length `n_components` + - If structured array, must have `n_components` fields. + + Returns + ------- + tuple of np.ndarray + + """ + if isinstance(data, np.ndarray) and data.dtype.fields is not None: + # structured array + if len(data.dtype.fields) != n_components: + raise ValueError( + "Structured array inputs to MultiNorm must have the same " + "number of fields as components in the MultiNorm. Expected " + f"{n_components}, but got {len(data.dtype.fields)} fields" + ) + else: + return tuple(data[field] for field in data.dtype.names) + try: + n_elements = len(data) + except TypeError: + raise ValueError("MultiNorm expects a sequence with one element per " + f"component as input, but got {data!r} instead") + if n_elements != n_components: + if isinstance(data, np.ndarray) and data.shape[-1] == n_components: + if len(data.shape) == 2: + raise ValueError( + f"MultiNorm expects a sequence with one element per component. " + "You can use `data_transposed = data.T` " + "to convert the input data of shape " + f"{data.shape} to a compatible shape {data.shape[::-1]}") + else: + raise ValueError( + f"MultiNorm expects a sequence with one element per component. " + "You can use `data_as_list = [data[..., i] for i in " + "range(data.shape[-1])]` to convert the input data of shape " + f" {data.shape} to a compatible list") + + raise ValueError( + "MultiNorm expects a sequence with one element per component. " + f"This MultiNorm has {n_components} components, but got a sequence " + f"with {n_elements} elements" + ) + + return tuple(data[i] for i in range(n_elements)) + + def rgb_to_hsv(arr): """ Convert an array of float RGB values (in the range [0, 1]) to HSV values. @@ -3294,11 +3653,10 @@ def rgb_to_hsv(arr): f"shape {arr.shape} was found.") in_shape = arr.shape - arr = np.array( - arr, copy=False, - dtype=np.promote_types(arr.dtype, np.float32), # Don't work on ints. - ndmin=2, # In case input was 1D. - ) + # ensure numerics are done at least on float32; ints are cast as well + arr = np.asarray(arr, dtype=np.promote_types(arr.dtype, np.float32)) + if arr.ndim == 1: + arr = np.expand_dims(arr, axis=0) # ensure arr is 2D out = np.zeros_like(arr) arr_max = arr.max(-1) diff --git a/lib/matplotlib/colors.pyi b/lib/matplotlib/colors.pyi index cdc6e5e7d89f..07bf01b8f995 100644 --- a/lib/matplotlib/colors.pyi +++ b/lib/matplotlib/colors.pyi @@ -270,6 +270,9 @@ class Norm(ABC): def autoscale_None(self, A: ArrayLike) -> None: ... @abstractmethod def scaled(self) -> bool: ... + @abstractmethod + @property + def n_components(self) -> int: ... class Normalize(Norm): @@ -305,6 +308,8 @@ class Normalize(Norm): def autoscale(self, A: ArrayLike) -> None: ... def autoscale_None(self, A: ArrayLike) -> None: ... def scaled(self) -> bool: ... + @property + def n_components(self) -> Literal[1]: ... class TwoSlopeNorm(Normalize): def __init__( @@ -409,6 +414,44 @@ class BoundaryNorm(Normalize): class NoNorm(Normalize): ... +class MultiNorm(Norm): + # Here "type: ignore[override]" is used for functions with a return type + # that differs from the function in the base class. + # i.e. where `MultiNorm` returns a tuple and Normalize returns a `float` etc. + def __init__( + self, + norms: ArrayLike, + vmin: ArrayLike | None = ..., + vmax: ArrayLike | None = ..., + clip: ArrayLike | None = ... + ) -> None: ... + @property + def norms(self) -> tuple[Normalize, ...]: ... + @property # type: ignore[override] + def vmin(self) -> tuple[float | None, ...]: ... + @vmin.setter + def vmin(self, values: ArrayLike | None) -> None: ... + @property # type: ignore[override] + def vmax(self) -> tuple[float | None, ...]: ... + @vmax.setter + def vmax(self, valued: ArrayLike | None) -> None: ... + @property # type: ignore[override] + def clip(self) -> tuple[bool, ...]: ... + @clip.setter + def clip(self, values: ArrayLike | None) -> None: ... + @overload + def __call__(self, values: tuple[np.ndarray, ...], clip: ArrayLike | bool | None = ...) -> tuple[np.ndarray, ...]: ... + @overload + def __call__(self, values: tuple[float, ...], clip: ArrayLike | bool | None = ...) -> tuple[float, ...]: ... + @overload + def __call__(self, values: ArrayLike, clip: ArrayLike | bool | None = ...) -> tuple: ... + def inverse(self, values: ArrayLike) -> tuple: ... # type: ignore[override] + def autoscale(self, A: ArrayLike) -> None: ... + def autoscale_None(self, A: ArrayLike) -> None: ... + def scaled(self) -> bool: ... + @property + def n_components(self) -> int: ... + def rgb_to_hsv(arr: ArrayLike) -> np.ndarray: ... def hsv_to_rgb(hsv: ArrayLike) -> np.ndarray: ... diff --git a/lib/matplotlib/container.py b/lib/matplotlib/container.py index b6dd43724f34..96b14cfd26f7 100644 --- a/lib/matplotlib/container.py +++ b/lib/matplotlib/container.py @@ -73,6 +73,48 @@ def __init__(self, patches, errorbar=None, *, datavalues=None, self.orientation = orientation super().__init__(patches, **kwargs) + @property + def bottoms(self): + """ + Return the values at the lower end of the bars. + + .. versionadded:: 3.11 + """ + if self.orientation == 'vertical': + return [p.get_y() for p in self.patches] + elif self.orientation == 'horizontal': + return [p.get_x() for p in self.patches] + else: + raise ValueError("orientation must be 'vertical' or 'horizontal'.") + + @property + def tops(self): + """ + Return the values at the upper end of the bars. + + .. versionadded:: 3.11 + """ + if self.orientation == 'vertical': + return [p.get_y() + p.get_height() for p in self.patches] + elif self.orientation == 'horizontal': + return [p.get_x() + p.get_width() for p in self.patches] + else: + raise ValueError("orientation must be 'vertical' or 'horizontal'.") + + @property + def position_centers(self): + """ + Return the centers of bar positions. + + .. versionadded:: 3.11 + """ + if self.orientation == 'vertical': + return [p.get_x() + p.get_width() / 2 for p in self.patches] + elif self.orientation == 'horizontal': + return [p.get_y() + p.get_height() / 2 for p in self.patches] + else: + raise ValueError("orientation must be 'vertical' or 'horizontal'.") + class ErrorbarContainer(Container): """ @@ -106,6 +148,78 @@ def __init__(self, lines, has_xerr=False, has_yerr=False, **kwargs): super().__init__(lines, **kwargs) +class PieContainer: + """ + Container for the artists of pie charts (e.g. created by `.Axes.pie`). + + .. versionadded:: 3.11 + + .. warning:: + The class name ``PieContainer`` name is provisional and may change in future + to reflect development of its functionality. + + You can access the wedge patches and further parameters by the attributes. + + Attributes + ---------- + wedges : list of `~matplotlib.patches.Wedge` + The artists of the pie wedges. + + values : `numpy.ndarray` + The data that the pie is based on. + + fracs : `numpy.ndarray` + The fraction of the pie that each wedge represents. + + texts : list of list of `~matplotlib.text.Text` + The artists of any labels on the pie wedges. Each inner list has one + text label per wedge. + + """ + def __init__(self, wedges, values, normalize): + self.wedges = wedges + self._texts = [] + self._values = values + self._normalize = normalize + + @property + def texts(self): + # Only return non-empty sublists. An empty sublist may have been added + # for backwards compatibility of the Axes.pie return value (see __getitem__). + return [t_list for t_list in self._texts if t_list] + + @property + def values(self): + result = self._values.copy() + result.flags.writeable = False + return result + + @property + def fracs(self): + if self._normalize: + result = self._values / self._values.sum() + else: + result = self._values + + result.flags.writeable = False + return result + + def add_texts(self, texts): + """Add a list of `~matplotlib.text.Text` objects to the container.""" + self._texts.append(texts) + + def remove(self): + """Remove all wedges and texts from the axes""" + for artist_list in self.wedges, self._texts: + for artist in cbook.flatten(artist_list): + artist.remove() + + def __getitem__(self, key): + # needed to support unpacking into a tuple for backward compatibility of the + # Axes.pie return value + return (self.wedges, *self._texts)[key] + + class StemContainer(Container): """ Container for the artists created in a :meth:`.Axes.stem` plot. diff --git a/lib/matplotlib/container.pyi b/lib/matplotlib/container.pyi index c66e7ba4b4c3..772801b16d6d 100644 --- a/lib/matplotlib/container.pyi +++ b/lib/matplotlib/container.pyi @@ -1,11 +1,13 @@ from matplotlib.artist import Artist from matplotlib.lines import Line2D from matplotlib.collections import LineCollection -from matplotlib.patches import Rectangle +from matplotlib.patches import Rectangle, Wedge +from matplotlib.text import Text from collections.abc import Callable from typing import Any, Literal from numpy.typing import ArrayLike +from numpy import ndarray class Container(tuple): def __new__(cls, *args, **kwargs): ... @@ -32,6 +34,12 @@ class BarContainer(Container): orientation: Literal["vertical", "horizontal"] | None = ..., **kwargs ) -> None: ... + @property + def bottoms(self) -> list[float]: ... + @property + def tops(self) -> list[float]: ... + @property + def position_centers(self) -> list[float]: ... class ErrorbarContainer(Container): lines: tuple[Line2D, tuple[Line2D, ...], tuple[LineCollection, ...]] @@ -45,6 +53,24 @@ class ErrorbarContainer(Container): **kwargs ) -> None: ... +class PieContainer(Container): + wedges: list[Wedge] + def __init__( + self, + wedges: list[Wedge], + values: ndarray, + normalize: bool, + ) -> None: ... + @property + def texts(self) -> list[list[Text]]: ... + @property + def values(self) -> ndarray: ... + @property + def fracs(self) -> ndarray: ... + def add_texts(self, + texts: list[Text], + ) -> None: ... + class StemContainer(Container): markerline: Line2D stemlines: LineCollection diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 8c5c9b566441..dfc39ed664f9 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -76,7 +76,7 @@ def clabel(self, levels=None, *, a subset of ``cs.levels``. If not given, all levels are labeled. fontsize : str or float, default: :rc:`font.size` - Size in points or relative size e.g., 'smaller', 'x-large'. + Size in points or relative size e.g., 'small', 'x-large'. See `.Text.set_size` for accepted string values. colors : :mpltype:`color` or colors or None, default: None @@ -602,7 +602,7 @@ def __init__(self, ax, *args, hatches=(None,), alpha=None, origin=None, extent=None, cmap=None, colors=None, norm=None, vmin=None, vmax=None, colorizer=None, extend='neither', antialiased=None, nchunk=0, - locator=None, transform=None, negative_linestyles=None, clip_path=None, + locator=None, transform=None, negative_linestyles=None, **kwargs): """ Draw contour lines or filled regions, depending on @@ -656,7 +656,6 @@ def __init__(self, ax, *args, super().__init__( antialiaseds=antialiased, alpha=alpha, - clip_path=clip_path, transform=transform, colorizer=colorizer, ) @@ -672,6 +671,9 @@ def __init__(self, ax, *args, self.nchunk = nchunk self.locator = locator + if "color" in kwargs: + raise _api.kwarg_error("ContourSet.__init__", "color") + if colorizer: self._set_colorizer_check_keywords(colorizer, cmap=cmap, norm=norm, vmin=vmin, @@ -733,13 +735,12 @@ def __init__(self, ax, *args, i0 = 1 cmap = mcolors.ListedColormap( - cbook._resize_sequence(color_sequence[i0:], ncolors)) - - if use_set_under_over: - if self._extend_min: - cmap.set_under(color_sequence[0]) - if self._extend_max: - cmap.set_over(color_sequence[-1]) + cbook._resize_sequence(color_sequence[i0:], ncolors), + under=(color_sequence[0] + if use_set_under_over and self._extend_min else None), + over=(color_sequence[-1] + if use_set_under_over and self._extend_max else None), + ) # label lists must be initialized here self.labelTexts = [] @@ -764,23 +765,18 @@ def __init__(self, ax, *args, _api.warn_external('linewidths is ignored by contourf') # Lower and upper contour levels. lowers, uppers = self._get_lowers_and_uppers() - self.set( - edgecolor="none", - # Default zorder taken from Collection - zorder=kwargs.pop("zorder", 1), - rasterized=kwargs.pop("rasterized", False), - ) - + self.set(edgecolor="none") else: self.set( facecolor="none", linewidths=self._process_linewidths(linewidths), linestyle=self._process_linestyles(linestyles), + label="_nolegend_", # Default zorder taken from LineCollection, which is higher # than for filled contours so that lines are displayed on top. - zorder=kwargs.pop("zorder", 2), - label="_nolegend_", + zorder=2, ) + self.set(**kwargs) # Let user-set values override defaults. self.axes.add_collection(self, autolim=False) self.sticky_edges.x[:] = [self._mins[0], self._maxs[0]] @@ -790,12 +786,6 @@ def __init__(self, ax, *args, self.changed() # set the colors - if kwargs: - _api.warn_external( - 'The following kwargs were not used by contour: ' + - ", ".join(map(repr, kwargs)) - ) - allsegs = property(lambda self: [ [subp.vertices for subp in p._iter_connected_components()] for p in self.get_paths()]) @@ -1278,7 +1268,8 @@ def draw(self, renderer): if edgecolors.size == 0: edgecolors = ("none",) for idx in range(n_paths): - with cbook._setattr_cm(self, _paths=[paths[idx]]), self._cm_set( + with self._cm_set( + paths=[paths[idx]], hatch=self.hatches[idx % len(self.hatches)], array=[self.get_array()[idx]], linewidths=[self.get_linewidths()[idx % len(self.get_linewidths())]], @@ -1342,7 +1333,7 @@ def _process_args(self, *args, corner_mask=None, algorithm=None, **kwargs): # if the transform is not trans data, and some part of it # contains transData, transform the xs and ys to data coordinates if (t != self.axes.transData and - any(t.contains_branch_seperately(self.axes.transData))): + any(t.contains_branch_separately(self.axes.transData))): trans_to_data = t - self.axes.transData pts = np.vstack([x.flat, y.flat]).T transformed_pts = trans_to_data.transform(pts) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 511e1c6df6cc..6a2bd0194bd4 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -537,7 +537,7 @@ def drange(dstart, dend, delta): # calculate end of the interval which will be generated dinterval_end = dstart + num * delta - # ensure, that an half open interval will be generated [dstart, dend) + # ensure, that a half open interval will be generated [dstart, dend) if dinterval_end >= dend: # if the endpoint is greater than or equal to dend, # just subtract one delta diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 9e8b6a5facf5..f07157a63524 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -26,12 +26,15 @@ import subprocess import sys from collections import namedtuple -from functools import cache, lru_cache, partial, wraps +from functools import cache, cached_property, lru_cache, partial, wraps from pathlib import Path +import fontTools.agl import numpy as np +import matplotlib as mpl from matplotlib import _api, cbook, font_manager +from matplotlib.ft2font import LoadFlags _log = logging.getLogger(__name__) @@ -67,42 +70,15 @@ class Text(namedtuple('Text', 'x y font glyph width')): """ A glyph in the dvi file. - The *x* and *y* attributes directly position the glyph. The *font*, - *glyph*, and *width* attributes are kept public for back-compatibility, - but users wanting to draw the glyph themselves are encouraged to instead - load the font specified by `font_path` at `font_size`, warp it with the - effects specified by `font_effects`, and load the glyph at the FreeType - glyph `index`. - """ - - def _get_pdftexmap_entry(self): - return PsfontsMap(find_tex_file("pdftex.map"))[self.font.texname] - - @property - def font_path(self): - """The `~pathlib.Path` to the font for this glyph.""" - psfont = self._get_pdftexmap_entry() - if psfont.filename is None: - raise ValueError("No usable font file found for {} ({}); " - "the font may lack a Type-1 version" - .format(psfont.psname.decode("ascii"), - psfont.texname.decode("ascii"))) - return Path(psfont.filename) - - @property - def font_size(self): - """The font size.""" - return self.font.size + In order to render the glyph, load the glyph at index ``text.index`` + from the font at ``text.font.resolve_path()`` with size ``text.font.size``, + warped with ``text.font.effects``, then draw it at position + ``(text.x, text.y)``. - @property - def font_effects(self): - """ - The "font effects" dict for this glyph. - - This dict contains the values for this glyph of SlantFont and - ExtendFont (if any), read off :file:`pdftex.map`. - """ - return self._get_pdftexmap_entry().effects + ``text.glyph`` is the glyph number actually stored in the dvi file (whose + interpretation depends on the font). ``text.width`` is the glyph width in + dvi units. + """ @property def index(self): @@ -112,25 +88,57 @@ def index(self): # See DviFont._index_dvi_to_freetype for details on the index mapping. return self.font._index_dvi_to_freetype(self.glyph) - @property # To be deprecated together with font_size, font_effects. + font_path = property(lambda self: self.font.resolve_path()) + font_size = property(lambda self: self.font.size) + font_effects = property(lambda self: self.font.effects) + + @property # To be deprecated together with font_path, font_size, font_effects. def glyph_name_or_index(self): """ - Either the glyph name or the native charmap glyph index. - - If :file:`pdftex.map` specifies an encoding for this glyph's font, that - is a mapping of glyph indices to Adobe glyph names; use it to convert - dvi indices to glyph names. Callers can then convert glyph names to - glyph indices (with FT_Get_Name_Index/get_name_index), and load the - glyph using FT_Load_Glyph/load_glyph. - - If :file:`pdftex.map` specifies no encoding, the indices directly map - to the font's "native" charmap; glyphs should directly load using - FT_Load_Char/load_char after selecting the native charmap. + The glyph name, the native charmap glyph index, or the raw glyph index. + + If the font is a TrueType file (which can currently only happen for + DVI files generated by xetex or luatex), then this number is the raw + index of the glyph, which can be passed to FT_Load_Glyph/load_glyph. + + Otherwise, the font is a PostScript font. For such fonts, if + :file:`pdftex.map` specifies an encoding for this glyph's font, + that is a mapping of glyph indices to Adobe glyph names; which + is used by this property to convert dvi numbers to glyph names. + Callers can then convert glyph names to glyph indices (with + FT_Get_Name_Index/get_name_index), and load the glyph using + FT_Load_Glyph/load_glyph. + + If :file:`pdftex.map` specifies no encoding for a PostScript font, + this number is an index to the font's "native" charmap; glyphs should + directly load using FT_Load_Char/load_char after selecting the native + charmap. """ + # The last section is only true on luatex since luaotfload 3.23; this + # must be checked by the code generated by texmanager. (luaotfload's + # docs states "No one should rely on the mapping between DVI character + # codes and font glyphs [prior to v3.15] unless they tightly + # control all involved versions and are deeply familiar with the + # implementation", but a further mapping bug was fixed in luaotfload + # commit 8f2dca4, first included in v3.23). entry = self._get_pdftexmap_entry() return (_parse_enc(entry.encoding)[self.glyph] if entry.encoding is not None else self.glyph) + def _as_unicode_or_name(self): + if self.font.subfont: + raise NotImplementedError("Indexing TTC fonts is not supported yet") + path = self.font.resolve_path() + if path.name.lower().endswith("pk"): + # PK fonts have no encoding information; report glyphs as ASCII but + # with a "?" to indicate that this is just a guess. + return (f"{chr(self.glyph)}?" if chr(self.glyph).isprintable() else + f"pk{self.glyph:#02x}") + face = font_manager.get_font(path) + glyph_name = face.get_glyph_name(self.index) + glyph_str = fontTools.agl.toUnicode(glyph_name) + return glyph_str or glyph_name + # Opcode argument parsing # @@ -408,7 +416,7 @@ def _put_char_real(self, char): scale = font._scale for x, y, f, g, w in font._vf[char].text: newf = DviFont(scale=_mul1220(scale, f._scale), - tfm=f._tfm, texname=f.texname, vf=f._vf) + metrics=f._metrics, texname=f.texname, vf=f._vf) self.text.append(Text(self.h + _mul1220(x, scale), self.v + _mul1220(y, scale), newf, g, newf._width_of(g))) @@ -504,10 +512,21 @@ def _fnt_def(self, k, c, s, d, a, l): def _fnt_def_real(self, k, c, s, d, a, l): n = self.file.read(a + l) - fontname = n[-l:].decode('ascii') + fontname = n[-l:] + if fontname.startswith(b"[") and c == 0x4c756146: # c == "LuaF" + # See https://chat.stackexchange.com/rooms/106428 (and also + # https://tug.org/pipermail/dvipdfmx/2021-January/000168.html). + # AFAICT luatex's dvi drops info re: OpenType variation-axis values. + self.fonts[k] = DviFont.from_luatex(s, n) + return + fontname = fontname.decode("ascii") try: tfm = _tfmfile(fontname) except FileNotFoundError as exc: + if fontname.startswith("[") and fontname.endswith(";") and c == 0: + exc.add_note( + "This dvi file was likely generated with a too-old " + "version of luaotfload; luaotfload 3.23 is required.") # Explicitly allow defining missing fonts for Vf support; we only # register an error when trying to load a glyph from a missing font # and throw that error in Dvi._read. For Vf, _finalize_packet @@ -521,12 +540,12 @@ def _fnt_def_real(self, k, c, s, d, a, l): vf = _vffile(fontname) except FileNotFoundError: vf = None - self.fonts[k] = DviFont(scale=s, tfm=tfm, texname=n, vf=vf) + self.fonts[k] = DviFont(scale=s, metrics=tfm, texname=n, vf=vf) @_dispatch(247, state=_dvistate.pre, args=('u1', 'u4', 'u4', 'u4', 'u1')) def _pre(self, i, num, den, mag, k): self.file.read(k) # comment in the dvi file - if i != 2: + if i not in [2, 7]: # 2: pdftex, luatex; 7: xetex raise ValueError(f"Unknown dvi format {i}") if num != 25400000 or den != 7227 * 2**16: raise ValueError("Nonstandard units in dvi file") @@ -547,13 +566,66 @@ def _post(self, _): # TODO: actually read the postamble and finale? # currently post_post just triggers closing the file - @_dispatch(249) - def _post_post(self, _): + @_dispatch(249, args=()) + def _post_post(self): raise NotImplementedError - @_dispatch(min=250, max=255) - def _malformed(self, offset): - raise ValueError(f"unknown command: byte {250 + offset}") + @_dispatch(250, args=()) + def _begin_reflect(self): + raise NotImplementedError + + @_dispatch(251, args=()) + def _end_reflect(self): + raise NotImplementedError + + @_dispatch(252, args=()) + def _define_native_font(self): + k = self._read_arg(4, signed=False) + s = self._read_arg(4, signed=False) + flags = self._read_arg(2, signed=False) + l = self._read_arg(1, signed=False) + n = self.file.read(l) + i = self._read_arg(4, signed=False) + effects = {} + if flags & 0x0200: + effects["rgba"] = [self._read_arg(1, signed=False) for _ in range(4)] + if flags & 0x1000: + effects["extend"] = self._read_arg(4, signed=True) / 65536 + if flags & 0x2000: + effects["slant"] = self._read_arg(4, signed=True) / 65536 + if flags & 0x4000: + effects["embolden"] = self._read_arg(4, signed=True) / 65536 + self.fonts[k] = DviFont.from_xetex(s, n, i, effects) + + @_dispatch(253, args=()) + def _set_glyphs(self): + w = self._read_arg(4, signed=False) + k = self._read_arg(2, signed=False) + xy = [self._read_arg(4, signed=True) for _ in range(2 * k)] + g = [self._read_arg(2, signed=False) for _ in range(k)] + font = self.fonts[self.f] + for i in range(k): + self.text.append(Text(self.h + xy[2 * i], self.v + xy[2 * i + 1], + font, g[i], font._width_of(g[i]))) + self.h += w + + @_dispatch(254, args=()) + def _set_text_and_glyphs(self): + l = self._read_arg(2, signed=False) + t = self.file.read(2 * l) # utf16 + w = self._read_arg(4, signed=False) + k = self._read_arg(2, signed=False) + xy = [self._read_arg(4, signed=True) for _ in range(2 * k)] + g = [self._read_arg(2, signed=False) for _ in range(k)] + font = self.fonts[self.f] + for i in range(k): + self.text.append(Text(self.h + xy[2 * i], self.v + xy[2 * i + 1], + font, g[i], font._width_of(g[i]))) + self.h += w + + @_dispatch(255) + def _malformed(self, raw): + raise ValueError("unknown command: byte 255") class DviFont: @@ -571,10 +643,10 @@ class DviFont: ---------- scale : float Factor by which the font is scaled from its natural size. - tfm : Tfm + metrics : Tfm | TtfMetrics TeX font metrics for this font texname : bytes - Name of the font as used internally by TeX and friends, as an ASCII + Name of the font as used internally in the DVI file, as an ASCII bytestring. This is usually very different from any external font names; `PsfontsMap` can be used to find the external name of the font. vf : Vf @@ -590,17 +662,54 @@ class DviFont: Size of the font in Adobe points, converted from the slightly smaller TeX points. """ - __slots__ = ('texname', 'size', '_scale', '_vf', '_tfm', '_encoding') - def __init__(self, scale, tfm, texname, vf): + def __init__(self, scale, metrics, texname, vf): _api.check_isinstance(bytes, texname=texname) self._scale = scale - self._tfm = tfm + self._metrics = metrics self.texname = texname self._vf = vf - self.size = scale * (72.0 / (72.27 * 2**16)) + self._path = None self._encoding = None + @classmethod + def from_luatex(cls, scale, texname): + path_b, sep, rest = texname[1:].rpartition(b"]") + if not (texname.startswith(b"[") and sep and rest[:1] in [b"", b":"]): + raise ValueError(f"Invalid modern font name: {texname}") + # utf8 on Windows, not utf16! + path = path_b.decode("utf8") if os.name == "nt" else os.fsdecode(path_b) + subfont = 0 + effects = {} + if rest[1:]: + for kv in rest[1:].decode("ascii").split(";"): + key, val = kv.split("=", 1) + if key == "index": + subfont = val + elif key in ["embolden", "slant", "extend"]: + effects[key] = int(val) / 65536 + else: + _log.warning("Ignoring invalid key-value pair: %r", kv) + metrics = TtfMetrics(path) + font = cls(scale, metrics, texname, vf=None) + font._path = Path(path) + font.subfont = subfont + font.effects = effects + return font + + @classmethod + def from_xetex(cls, scale, texname, subfont, effects): + # utf8 on Windows, not utf16! + path = texname.decode("utf8") if os.name == "nt" else os.fsdecode(texname) + metrics = TtfMetrics(path) + font = cls(scale, metrics, b"[" + texname + b"]", vf=None) + font._path = Path(path) + font.subfont = subfont + font.effects = effects + return font + + size = property(lambda self: self._scale * (72.0 / (72.27 * 2**16))) + widths = _api.deprecated("3.11")(property(lambda self: [ (1000 * self._tfm.width.get(char, 0)) >> 20 for char in range(max(self._tfm.width, default=-1) + 1)])) @@ -629,7 +738,7 @@ def __repr__(self): def _width_of(self, char): """Width of char in dvi units.""" - metrics = self._tfm.get_metrics(char) + metrics = self._metrics.get_metrics(char) if metrics is None: _log.debug('No width for char %d in font %s.', char, self.texname) return 0 @@ -637,7 +746,7 @@ def _width_of(self, char): def _height_depth_of(self, char): """Height and depth of char in dvi units.""" - metrics = self._tfm.get_metrics(char) + metrics = self._metrics.get_metrics(char) if metrics is None: _log.debug('No metrics for char %d in font %s', char, self.texname) return [0, 0] @@ -654,26 +763,55 @@ def _height_depth_of(self, char): hd[-1] = 0 return hd + def resolve_path(self): + if self._path is None: + fontmap = PsfontsMap(find_tex_file("pdftex.map")) + try: + psfont = fontmap[self.texname] + except LookupError as exc: + try: + find_tex_file(f"{self.texname.decode('ascii')}.mf") + except FileNotFoundError: + raise exc from None + else: + self._path = Path(find_tex_file( + f"{self.texname.decode('ascii')}.600pk")) + else: + if psfont.filename is None: + raise ValueError("No usable font file found for {} ({}); " + "the font may lack a Type-1 version" + .format(psfont.psname.decode("ascii"), + psfont.texname.decode("ascii"))) + self._path = Path(psfont.filename) + return self._path + + @cached_property + def subfont(self): + return 0 + + @cached_property + def effects(self): + if self.resolve_path().match("*.600pk"): + return {} + return PsfontsMap(find_tex_file("pdftex.map"))[self.texname].effects + def _index_dvi_to_freetype(self, idx): """Convert dvi glyph indices to FreeType ones.""" # Glyphs indices stored in the dvi file map to FreeType glyph indices # (i.e., which can be passed to FT_Load_Glyph) in various ways: + # - for xetex & luatex "native fonts", dvi indices are directly equal + # to FreeType indices. # - if pdftex.map specifies an ".enc" file for the font, that file maps # dvi indices to Adobe glyph names, which can then be converted to # FreeType glyph indices with FT_Get_Name_Index. # - if no ".enc" file is specified, then the font must be a Type 1 # font, and dvi indices directly index into the font's CharStrings # vector. - # - (xetex & luatex, currently unsupported, can also declare "native - # fonts", for which dvi indices are equal to FreeType indices.) + if self.texname.startswith(b"["): + return idx if self._encoding is None: + face = font_manager.get_font(self.resolve_path()) psfont = PsfontsMap(find_tex_file("pdftex.map"))[self.texname] - if psfont.filename is None: - raise ValueError("No usable font file found for {} ({}); " - "the font may lack a Type-1 version" - .format(psfont.psname.decode("ascii"), - psfont.texname.decode("ascii"))) - face = font_manager.get_font(psfont.filename) if psfont.encoding: self._encoding = [face.get_name_index(name) for name in _parse_enc(psfont.encoding)] @@ -882,6 +1020,27 @@ def get_metrics(self, idx): property(lambda self: {c: m.tex_depth for c, m in self._glyph_metrics})) +class TtfMetrics: + def __init__(self, filename): + self._face = font_manager.get_font(filename, hinting_factor=1) + + def get_metrics(self, idx): + # _mul1220 uses a truncating bitshift for compatibility with dvitype. + # When upem is 2048 the conversion to 12.20 is exact, but when + # upem is 1000 (e.g. lmroman10-regular.otf) the metrics themselves + # are not exactly representable as 12.20 fp. Manual testing via + # \sbox0{x}\count0=\wd0\typeout{\the\count0} suggests that metrics + # are rounded (not truncated) after conversion to 12.20 and before + # multiplication by the scale. + upem = self._face.units_per_EM # Usually 2048 or 1000. + g = self._face.load_glyph(idx, LoadFlags.NO_SCALE) + return TexMetrics( + tex_width=round(g.horiAdvance / upem * 2**20), + tex_height=round(g.horiBearingY / upem * 2**20), + tex_depth=round((g.height - g.horiBearingY) / upem * 2**20), + ) + + PsFont = namedtuple('PsFont', 'texname psname effects encoding filename') @@ -1094,9 +1253,12 @@ def __new__(cls): def _new_proc(self): return subprocess.Popen( - ["luatex", "--luaonly", - str(cbook._get_data_path("kpsewhich.lua"))], - stdin=subprocess.PIPE, stdout=subprocess.PIPE) + ["luatex", "--luaonly", str(cbook._get_data_path("kpsewhich.lua"))], + # mktexpk logs to stderr; suppress that. + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, + # Store generated pk fonts in our own cache. + env={"MT_VARTEXFONTS": str(Path(mpl.get_cachedir(), "vartexfonts")), + **os.environ}) def search(self, filename): if self._proc.poll() is not None: # Dead, restart it. @@ -1148,13 +1310,16 @@ def find_tex_file(filename): kwargs = {'env': {**os.environ, 'command_line_encoding': 'utf-8'}, 'encoding': 'utf-8'} else: # On POSIX, run through the equivalent of os.fsdecode(). - kwargs = {'encoding': sys.getfilesystemencoding(), + kwargs = {'env': {**os.environ}, + 'encoding': sys.getfilesystemencoding(), 'errors': 'surrogateescape'} + kwargs['env'].update( + MT_VARTEXFONTS=str(Path(mpl.get_cachedir(), "vartexfonts"))) try: - path = (cbook._check_and_log_subprocess(['kpsewhich', filename], - _log, **kwargs) - .rstrip('\n')) + path = cbook._check_and_log_subprocess( + ['kpsewhich', '-mktex=pk', filename], _log, **kwargs, + ).rstrip('\n') except (FileNotFoundError, RuntimeError): path = None @@ -1179,35 +1344,35 @@ def _fontfile(cls, suffix, texname): import itertools from argparse import ArgumentParser - import fontTools.agl - - from matplotlib.ft2font import FT2Font - parser = ArgumentParser() parser.add_argument("filename") parser.add_argument("dpi", nargs="?", type=float, default=None) + parser.add_argument("-d", "--debug", action="store_true") args = parser.parse_args() + if args.debug: + logging.basicConfig(level=logging.DEBUG) + def _print_fields(*args): print(" ".join(map("{:>11}".format, args))) with Dvi(args.filename, args.dpi) as dvi: - fontmap = PsfontsMap(find_tex_file('pdftex.map')) for page in dvi: print(f"=== NEW PAGE === " f"(w: {page.width}, h: {page.height}, d: {page.descent})") print("--- GLYPHS ---") - for font, group in itertools.groupby( - page.text, lambda text: text.font): - psfont = fontmap[font.texname] - fontpath = psfont.filename - print(f"font: {font.texname.decode('latin-1')} " - f"(scale: {font._scale / 2 ** 20}) at {fontpath}") - face = FT2Font(fontpath) + for font, group in itertools.groupby(page.text, lambda text: text.font): + font_name = (font.texname.decode("utf8") if os.name == "nt" + else os.fsdecode(font.texname)) + if isinstance(font._metrics, Tfm): + print(f"font: {font_name} at {font.resolve_path()}") + else: + print(f"font: {font_name}") + print(f"scale: {font._scale / 2 ** 20}") _print_fields("x", "y", "glyph", "chr", "w") for text in group: - glyph_str = fontTools.agl.toUnicode(face.get_glyph_name(text.index)) - _print_fields(text.x, text.y, text.glyph, glyph_str, text.width) + _print_fields(text.x, text.y, text.glyph, + text._as_unicode_or_name(), text.width) if page.boxes: print("--- BOXES ---") _print_fields("x", "y", "h", "w") diff --git a/lib/matplotlib/dviread.pyi b/lib/matplotlib/dviread.pyi index 12a9215b5308..1c24ff1c28a9 100644 --- a/lib/matplotlib/dviread.pyi +++ b/lib/matplotlib/dviread.pyi @@ -6,7 +6,7 @@ from enum import Enum from collections.abc import Generator from typing import NamedTuple -from typing_extensions import Self # < Py 3.11 +from typing import Self class _dvistate(Enum): pre = ... @@ -58,16 +58,28 @@ class Dvi: class DviFont: texname: bytes - size: float def __init__( - self, scale: float, tfm: Tfm, texname: bytes, vf: Vf | None + self, scale: float, metrics: Tfm | TtfMetrics, texname: bytes, vf: Vf | None ) -> None: ... + @classmethod + def from_luatex(cls, scale: float, texname: bytes) -> DviFont: ... + @classmethod + def from_xetex( + cls, scale: float, texname: bytes, subfont: int, effects: dict[str, float] + ) -> DviFont: ... def __eq__(self, other: object) -> bool: ... def __ne__(self, other: object) -> bool: ... @property + def size(self) -> float: ... + @property def widths(self) -> list[int]: ... @property def fname(self) -> str: ... + def resolve_path(self) -> Path: ... + @property + def subfont(self) -> int: ... + @property + def effects(self) -> dict[str, float]: ... class Vf(Dvi): def __init__(self, filename: str | os.PathLike) -> None: ... @@ -93,6 +105,10 @@ class Tfm: @property def depth(self) -> dict[int, int]: ... +class TtfMetrics: + def __init__(self, filename: str | os.PathLike) -> None: ... + def get_metrics(self, idx: int) -> TexMetrics: ... + class PsFont(NamedTuple): texname: bytes psname: bytes diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 03549dd53bc1..85b378fcd422 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -2642,7 +2642,7 @@ def __init__(self, self._set_artist_props(self.patch) self.patch.set_antialiased(False) - FigureCanvasBase(self) # Set self.canvas. + self._set_base_canvas() if subplotpars is None: subplotpars = SubplotParams() @@ -2766,7 +2766,7 @@ def show(self, warn=True): .. warning:: - This does not manage an GUI event loop. Consequently, the figure + This does not manage a GUI event loop. Consequently, the figure may only be shown briefly or not shown at all if you or your environment are not managing an event loop. @@ -2996,6 +2996,18 @@ def get_constrained_layout_pads(self, relative=False): return w_pad, h_pad, wspace, hspace + def _set_base_canvas(self): + """ + Initialize self.canvas with a FigureCanvasBase instance. + + This is used upon initialization of the Figure, but also + to reset the canvas when decoupling from pyplot. + """ + FigureCanvasBase(self) # Set self.canvas as a side-effect + # undo any high-dpi scaling + if self._dpi != self._original_dpi: + self.dpi = self._original_dpi + def set_canvas(self, canvas): """ Set the canvas that contains the figure @@ -3309,8 +3321,9 @@ def __setstate__(self, state): self.__dict__ = state # re-initialise some of the unstored state information - FigureCanvasBase(self) # Set self.canvas. - + self._set_base_canvas() + # force the bounding boxes to respect current dpi + self.dpi_scale_trans.clear().scale(self._dpi) if restore_to_pylab: # lazy import to avoid circularity import matplotlib.pyplot as plt diff --git a/lib/matplotlib/figure.pyi b/lib/matplotlib/figure.pyi index 61dc79619a80..59d276362dc5 100644 --- a/lib/matplotlib/figure.pyi +++ b/lib/matplotlib/figure.pyi @@ -27,7 +27,7 @@ from matplotlib.text import Text from matplotlib.transforms import Affine2D, Bbox, BboxBase, Transform from mpl_toolkits.mplot3d import Axes3D -from .typing import ColorType, HashableList +from .typing import ColorType, HashableList, LegendLocType _T = TypeVar("_T") @@ -152,13 +152,16 @@ class FigureBase(Artist): @overload def legend(self) -> Legend: ... @overload - def legend(self, handles: Iterable[Artist], labels: Iterable[str], **kwargs) -> Legend: ... + def legend(self, handles: Iterable[Artist], labels: Iterable[str], + *, loc: LegendLocType | None = ..., **kwargs) -> Legend: ... @overload - def legend(self, *, handles: Iterable[Artist], **kwargs) -> Legend: ... + def legend(self, *, handles: Iterable[Artist], + loc: LegendLocType | None = ..., **kwargs) -> Legend: ... @overload - def legend(self, labels: Iterable[str], **kwargs) -> Legend: ... + def legend(self, labels: Iterable[str], + *, loc: LegendLocType | None = ..., **kwargs) -> Legend: ... @overload - def legend(self, **kwargs) -> Legend: ... + def legend(self, *, loc: LegendLocType | None = ..., **kwargs) -> Legend: ... def text( self, @@ -191,11 +194,24 @@ class FigureBase(Artist): def align_labels(self, axs: Iterable[Axes] | None = ...) -> None: ... def add_gridspec(self, nrows: int = ..., ncols: int = ..., **kwargs) -> GridSpec: ... @overload + def subfigures( + self, + nrows: int, + ncols: int, + squeeze: Literal[False], + wspace: float | None = ..., + hspace: float | None = ..., + width_ratios: ArrayLike | None = ..., + height_ratios: ArrayLike | None = ..., + **kwargs + ) -> np.ndarray: ... + @overload def subfigures( self, nrows: int = ..., ncols: int = ..., - squeeze: Literal[False] = ..., + *, + squeeze: Literal[False], wspace: float | None = ..., hspace: float | None = ..., width_ratios: ArrayLike | None = ..., diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 2db98b75ab2e..98361eaa01e8 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -35,7 +35,7 @@ from io import BytesIO import json import logging -from numbers import Number +from numbers import Integral import os from pathlib import Path import plistlib @@ -172,6 +172,10 @@ ] +def _normalize_weight(weight): + return weight if isinstance(weight, Integral) else weight_dict[weight] + + def get_fontext_synonyms(fontext): """ Return a list of file extensions that are synonyms for @@ -744,7 +748,7 @@ def get_variant(self): def get_weight(self): """ - Set the font weight. Options are: A numeric value in the + Get the font weight. Options are: A numeric value in the range 0-1000 or one of 'light', 'normal', 'regular', 'book', 'medium', 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy', 'extra bold', 'black' @@ -1256,8 +1260,8 @@ def score_weight(self, weight1, weight2): # exact match of the weight names, e.g. weight1 == weight2 == "regular" if cbook._str_equal(weight1, weight2): return 0.0 - w1 = weight1 if isinstance(weight1, Number) else weight_dict[weight1] - w2 = weight2 if isinstance(weight2, Number) else weight_dict[weight2] + w1 = _normalize_weight(weight1) + w2 = _normalize_weight(weight2) return 0.95 * (abs(w1 - w2) / 1000) + 0.05 def score_size(self, size1, size2): @@ -1480,6 +1484,10 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, best_font = font if score == 0: break + if best_font is not None and (_normalize_weight(prop.get_weight()) != + _normalize_weight(best_font.weight)): + _log.warning('findfont: Failed to find font weight %s, now using %s.', + prop.get_weight(), best_font.weight) if best_font is None or best_score >= 10.0: if fallback_to_default: diff --git a/lib/matplotlib/font_manager.pyi b/lib/matplotlib/font_manager.pyi index c64ddea3e073..e865f67384cd 100644 --- a/lib/matplotlib/font_manager.pyi +++ b/lib/matplotlib/font_manager.pyi @@ -1,14 +1,13 @@ +from collections.abc import Iterable from dataclasses import dataclass +from numbers import Integral import os +from pathlib import Path +from typing import Any, Literal from matplotlib._afm import AFM from matplotlib import ft2font -from pathlib import Path - -from collections.abc import Iterable -from typing import Any, Literal - font_scalings: dict[str | None, float] stretch_dict: dict[str, int] weight_dict: dict[str, int] @@ -19,6 +18,7 @@ MSUserFontDirectories: list[str] X11FontDirectories: list[str] OSXFontDirectories: list[str] +def _normalize_weight(weight: str | Integral) -> Integral: ... def get_fontext_synonyms(fontext: str) -> list[str]: ... def list_fonts(directory: str, extensions: Iterable[str]) -> list[str]: ... def win32FontDirectory() -> str: ... diff --git a/lib/matplotlib/inset.py b/lib/matplotlib/inset.py index fb5bfacff924..aae640db6f81 100644 --- a/lib/matplotlib/inset.py +++ b/lib/matplotlib/inset.py @@ -126,26 +126,40 @@ def set_linestyle(self, ls): """ Set the linestyle of the rectangle and the connectors. - ======================================================= ================ - linestyle description - ======================================================= ================ - ``'-'`` or ``'solid'`` solid line - ``'--'`` or ``'dashed'`` dashed line - ``'-.'`` or ``'dashdot'`` dash-dotted line - ``':'`` or ``'dotted'`` dotted line - ``''`` or ``'none'`` (discouraged: ``'None'``, ``' '``) draw nothing - ======================================================= ================ + Parameters + ---------- + ls : {'-', '--', '-.', ':', '', ...} or (offset, on-off-seq) + Possible values: - Alternatively a dash tuple of the following form can be provided:: + - A string: - (offset, onoffseq) + ======================================================= ================ + linestyle description + ======================================================= ================ + ``'-'`` or ``'solid'`` solid line + ``'--'`` or ``'dashed'`` dashed line + ``'-.'`` or ``'dashdot'`` dash-dotted line + ``':'`` or ``'dotted'`` dotted line + ``''`` or ``'none'`` (discouraged: ``'None'``, ``' '``) draw nothing + ======================================================= ================ - where ``onoffseq`` is an even length tuple of on and off ink in points. + - A tuple describing the start position and lengths of dashes and spaces: - Parameters - ---------- - ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...} - The line style. + (offset, onoffseq) + + where + + - *offset* is a float specifying the offset (in points); i.e. how much + is the dash pattern shifted. + - *onoffseq* is a sequence of on and off ink in points. There can be + arbitrary many pairs of on and off values. + + Example: The tuple ``(0, (10, 5, 1, 5))`` means that the pattern starts + at the beginning of the line. It draws a 10 point long dash, + then a 5 point long space, then a 1 point long dash, followed by a 5 point + long space, and then the pattern repeats. + + For examples see :doc:`/gallery/lines_bars_and_markers/linestyles`. """ self._shared_setter('linestyle', ls) diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 2fb14e52c58c..876725fbc4f6 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -37,7 +37,7 @@ from matplotlib.patches import (Patch, Rectangle, Shadow, FancyBboxPatch, StepPatch) from matplotlib.collections import ( - Collection, CircleCollection, LineCollection, PathCollection, + Collection, CircleCollection, LineCollection, PatchCollection, PathCollection, PolyCollection, RegularPolyCollection) from matplotlib.text import Text from matplotlib.transforms import Bbox, BboxBase, TransformedBbox @@ -196,6 +196,12 @@ def _update_bbox_to_anchor(self, loc_in_canvas): The legend's background patch edge color. If ``"inherit"``, use :rc:`axes.edgecolor`. +linewidth : float or None, default: :rc:`legend.linewidth` + The legend's background patch edge linewidth. + If ``None``, use :rc:`patch.linewidth`. + + .. versionadded:: 3.11 + mode : {"expand", None} If *mode* is set to ``"expand"`` the legend will be horizontally expanded to fill the Axes area (or *bbox_to_anchor* if defines @@ -297,8 +303,13 @@ def _update_bbox_to_anchor(self, loc_in_canvas): _loc_doc_best = """ The string ``'best'`` places the legend at the location, among the nine locations defined so far, with the minimum overlap with other drawn - artists. This option can be quite slow for plots with large amounts of - data; your plotting speed may benefit from providing a specific location. + artists. This currently takes into account most, but not all, artists + added to the Axes via plotting functions. In particular it does not consider + inset axes, titles, or axis labels. + + The computation of the best position can be expensive for plots with large + amounts of data. If speed becomes a concern, you may may benefit from + providing a specific location. """ _legend_kw_axes_st = ( @@ -385,6 +396,7 @@ def __init__( framealpha=None, # set frame alpha edgecolor=None, # frame patch edgecolor facecolor=None, # frame patch facecolor + linewidth=None, # frame patch linewidth bbox_to_anchor=None, # bbox to which the legend will be anchored bbox_transform=None, # transform for the bbox @@ -526,9 +538,12 @@ def __init__( fancybox = mpl._val_or_rc(fancybox, "legend.fancybox") + linewidth = mpl._val_or_rc(linewidth, "legend.linewidth") + self.legendPatch = FancyBboxPatch( xy=(0, 0), width=1, height=1, facecolor=facecolor, edgecolor=edgecolor, + linewidth=linewidth, # If shadow is used, default to alpha=1 (#8943). alpha=(framealpha if framealpha is not None else 1 if shadow @@ -576,7 +591,11 @@ def __init__( # set the text color color_getters = { # getter function depends on line or patch - 'linecolor': ['get_color', 'get_facecolor'], + 'linecolor': ['get_markerfacecolor', + 'get_facecolor', + 'get_markeredgecolor', + 'get_edgecolor', + 'get_color'], 'markerfacecolor': ['get_markerfacecolor', 'get_facecolor'], 'mfc': ['get_markerfacecolor', 'get_facecolor'], 'markeredgecolor': ['get_markeredgecolor', 'get_edgecolor'], @@ -595,19 +614,22 @@ def __init__( for getter_name in getter_names: try: color = getattr(handle, getter_name)() - if isinstance(color, np.ndarray): - if ( - color.shape[0] == 1 - or np.isclose(color, color[0]).all() - ): - text.set_color(color[0]) - else: - pass - else: - text.set_color(color) - break except AttributeError: - pass + continue + if isinstance(color, np.ndarray): + if color.size == 0: + continue + elif (color.shape[0] == 1 or np.isclose(color, color[0]).all()): + text.set_color(color[0]) + else: + pass + elif cbook._str_lower_equal(color, 'none'): + continue + elif mpl.colors.to_rgba(color)[3] == 0: + continue + else: + text.set_color(color) + break elif cbook._str_equal(labelcolor, 'none'): for text in self.texts: text.set_color(labelcolor) @@ -780,6 +802,7 @@ def draw(self, renderer): BarContainer: legend_handler.HandlerPatch( update_func=legend_handler.update_from_first_child), tuple: legend_handler.HandlerTuple(), + PatchCollection: legend_handler.HandlerPolyCollection(), PathCollection: legend_handler.HandlerPathCollection(), PolyCollection: legend_handler.HandlerPolyCollection() } @@ -1140,9 +1163,10 @@ def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer): parentbbox : `~matplotlib.transforms.Bbox` A parent box which will contain the bbox, in display coordinates. """ + pad = self.borderaxespad * renderer.points_to_pixels(self._fontsize) return offsetbox._get_anchored_bbox( loc, bbox, parentbbox, - self.borderaxespad * renderer.points_to_pixels(self._fontsize)) + pad, pad) def _find_best_position(self, width, height, renderer): """Determine the best location to place the legend.""" diff --git a/lib/matplotlib/legend.pyi b/lib/matplotlib/legend.pyi index dde5882da69d..e17738c76161 100644 --- a/lib/matplotlib/legend.pyi +++ b/lib/matplotlib/legend.pyi @@ -14,12 +14,13 @@ from matplotlib.transforms import ( BboxBase, Transform, ) +from matplotlib.typing import ColorType, LegendLocType import pathlib from collections.abc import Iterable from typing import Any, Literal, overload -from .typing import ColorType + class DraggableLegend(DraggableOffsetBox): legend: Legend @@ -55,7 +56,7 @@ class Legend(Artist): handles: Iterable[Artist | tuple[Artist, ...]], labels: Iterable[str], *, - loc: str | tuple[float, float] | int | None = ..., + loc: LegendLocType | None = ..., numpoints: int | None = ..., markerscale: float | None = ..., markerfirst: bool = ..., @@ -84,6 +85,7 @@ class Legend(Artist): framealpha: float | None = ..., edgecolor: Literal["inherit"] | ColorType | None = ..., facecolor: Literal["inherit"] | ColorType | None = ..., + linewidth: float | None = ..., bbox_to_anchor: BboxBase | tuple[float, float] | tuple[float, float, float, float] @@ -118,7 +120,7 @@ class Legend(Artist): def get_texts(self) -> list[Text]: ... def set_alignment(self, alignment: Literal["center", "left", "right"]) -> None: ... def get_alignment(self) -> Literal["center", "left", "right"]: ... - def set_loc(self, loc: str | tuple[float, float] | int | None = ...) -> None: ... + def set_loc(self, loc: LegendLocType | None = ...) -> None: ... def set_title( self, title: str, prop: FontProperties | str | pathlib.Path | None = ... ) -> None: ... diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 72c57bf77b5c..7c374843b5c1 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -183,7 +183,7 @@ def _slice_or_none(in_v, slc): if ax is None: raise ValueError( "markevery is specified relative to the Axes size, but " - "the line does not have a Axes as parent") + "the line does not have an Axes as parent") # calc cumulative distance along path (in display coords): fin = np.isfinite(verts).all(axis=1) @@ -1149,7 +1149,7 @@ def set_linestyle(self, ls): Parameters ---------- - ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...} + ls : {'-', '--', '-.', ':', '', ...} or (offset, on-off-seq) Possible values: - A string: @@ -1164,13 +1164,23 @@ def set_linestyle(self, ls): ``''`` or ``'none'`` (discouraged: ``'None'``, ``' '``) draw nothing ======================================================= ================ - - Alternatively a dash tuple of the following form can be - provided:: + - A tuple describing the start position and lengths of dashes and spaces: (offset, onoffseq) - where ``onoffseq`` is an even length tuple of on and off ink - in points. See also :meth:`set_dashes`. + where + + - *offset* is a float specifying the offset (in points); i.e. how much + is the dash pattern shifted. + - *onoffseq* is a sequence of on and off ink in points. There can be + arbitrary many pairs of on and off values. + + Example: The tuple ``(0, (10, 5, 1, 5))`` means that the pattern starts + at the beginning of the line. It draws a 10 point long dash, + then a 5 point long space, then a 1 point long dash, followed by a 5 point + long space, and then the pattern repeats. + + See also :meth:`set_dashes`. For examples see :doc:`/gallery/lines_bars_and_markers/linestyles`. """ diff --git a/lib/matplotlib/meson.build b/lib/matplotlib/meson.build index c4746f332bcb..c0bfdb227e2e 100644 --- a/lib/matplotlib/meson.build +++ b/lib/matplotlib/meson.build @@ -17,6 +17,7 @@ python_sources = [ '_mathtext.py', '_mathtext_data.py', '_pylab_helpers.py', + '_style_helpers.py', '_text_helpers.py', '_tight_bbox.py', '_tight_layout.py', diff --git a/lib/matplotlib/mlab.py b/lib/matplotlib/mlab.py index b4b4c3f96828..a694308384c1 100644 --- a/lib/matplotlib/mlab.py +++ b/lib/matplotlib/mlab.py @@ -212,16 +212,8 @@ def detrend_linear(y): def _stride_windows(x, n, noverlap=0): - x = np.asarray(x) - _api.check_isinstance(Integral, n=n, noverlap=noverlap) - if not (1 <= n <= x.size and n < noverlap): - raise ValueError(f'n ({n}) and noverlap ({noverlap}) must be positive integers ' - f'with n < noverlap and n <= x.size ({x.size})') - - if n == 1 and noverlap == 0: - return x[np.newaxis] - + x = np.asarray(x) step = n - noverlap shape = (n, (x.shape[-1]-noverlap)//step) strides = (x.strides[0], step*x.strides[0]) @@ -257,7 +249,7 @@ def _spectral_helper(x, y=None, NFFT=None, Fs=None, detrend_func=None, if NFFT is None: NFFT = 256 - if noverlap >= NFFT: + if not (0 <= noverlap < NFFT): raise ValueError('noverlap must be less than NFFT') if mode is None or mode == 'default': @@ -361,7 +353,7 @@ def _spectral_helper(x, y=None, NFFT=None, Fs=None, detrend_func=None, # the sampling frequency, if desired. Scale everything, except the DC # component and the NFFT/2 component: - # if we have a even number of frequencies, don't scale NFFT/2 + # if we have an even number of frequencies, don't scale NFFT/2 if not NFFT % 2: slc = slice(1, -1, None) # if we have an odd number, just don't scale DC diff --git a/lib/matplotlib/mpl-data/kpsewhich.lua b/lib/matplotlib/mpl-data/kpsewhich.lua index 8e9172a45082..dc526effeebe 100644 --- a/lib/matplotlib/mpl-data/kpsewhich.lua +++ b/lib/matplotlib/mpl-data/kpsewhich.lua @@ -1,3 +1,4 @@ -- see dviread._LuatexKpsewhich kpse.set_program_name("latex") -while true do print(kpse.lookup(io.read():gsub("\r", ""))); io.flush(); end +kpse.init_prog("", 600, "ljfour") +while true do print(kpse.lookup(io.read():gsub("\r", ""), {mktexpk=true})); io.flush(); end diff --git a/lib/matplotlib/mpl-data/matplotlibrc b/lib/matplotlib/mpl-data/matplotlibrc index ccc5de5e372c..17705fe60347 100644 --- a/lib/matplotlib/mpl-data/matplotlibrc +++ b/lib/matplotlib/mpl-data/matplotlibrc @@ -243,7 +243,7 @@ ## ## The font.variant property has two values: normal or small-caps. For ## TrueType fonts, which are scalable fonts, small-caps is equivalent -## to using a font size of 'smaller', or about 83 % of the current font +## to using a font size of 'small', or about 83 % of the current font ## size. ## ## The font.weight property has effectively 13 values: normal, bold, @@ -263,7 +263,7 @@ ## special text sizes tick labels, axes, labels, title, etc., see the rc ## settings for axes and ticks. Special text sizes can be defined ## relative to font.size, using the following values: xx-small, x-small, -## small, medium, large, x-large, xx-large, larger, or smaller +## small, medium, large, x-large, xx-large #font.family: sans-serif #font.style: normal @@ -562,6 +562,7 @@ #legend.framealpha: 0.8 # legend patch transparency #legend.facecolor: inherit # inherit from axes.facecolor; or color spec #legend.edgecolor: 0.8 # background patch boundary color +#legend.linewidth: None # line width of the legend frame, None means inherit from patch.linewidth #legend.fancybox: True # if True, use a rounded box for the # legend background, else a rectangle #legend.shadow: False # if True, give background a shadow effect @@ -599,6 +600,9 @@ # the pyplot interface before emitting a warning. # If less than one this feature is disabled. #figure.raise_window : True # Raise the GUI window to front when show() is called. + # If set to False, we currently do not take any further + # actions and whether the window appears on the front + # may depend on the GUI framework and window manager. ## The figure subplot parameters. All dimensions are a fraction of the figure width and height. #figure.subplot.left: 0.125 # the left side of the subplots of the figure diff --git a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle index 92624503f99e..cd636d65c7c8 100644 --- a/lib/matplotlib/mpl-data/stylelib/classic.mplstyle +++ b/lib/matplotlib/mpl-data/stylelib/classic.mplstyle @@ -61,7 +61,7 @@ hist.bins : 10 # # The font.variant property has two values: normal or small-caps. For # TrueType fonts, which are scalable fonts, small-caps is equivalent -# to using a font size of 'smaller', or about 83% of the current font +# to using a font size of 'small', or about 83% of the current font # size. # # The font.weight property has effectively 13 values: normal, bold, @@ -86,7 +86,7 @@ font.stretch : normal # special text sizes tick labels, axes, labels, title, etc, see the rc # settings for axes and ticks. Special text sizes can be defined # relative to font.size, using the following values: xx-small, x-small, -# small, medium, large, x-large, xx-large, larger, or smaller +# small, medium, large, x-large, xx-large font.size : 12.0 font.serif : DejaVu Serif, New Century Schoolbook, Century Schoolbook L, Utopia, ITC Bookman, Bookman, Nimbus Roman No9 L, Times New Roman, Times, Palatino, Charter, serif font.sans-serif: DejaVu Sans, Lucida Grande, Verdana, Geneva, Lucid, Arial, Helvetica, Avant Garde, sans-serif diff --git a/lib/matplotlib/offsetbox.py b/lib/matplotlib/offsetbox.py index 974cc4f2db05..39035e0b785a 100644 --- a/lib/matplotlib/offsetbox.py +++ b/lib/matplotlib/offsetbox.py @@ -946,8 +946,13 @@ def __init__(self, loc, *, See the parameter *loc* of `.Legend` for details. pad : float, default: 0.4 Padding around the child as fraction of the fontsize. - borderpad : float, default: 0.5 + borderpad : float or (float, float), default: 0.5 Padding between the offsetbox frame and the *bbox_to_anchor*. + If a float, the same padding is used for both x and y. + If a tuple of two floats, it specifies the (x, y) padding. + + .. versionadded:: 3.11 + The *borderpad* parameter now accepts a tuple of (x, y) paddings. child : `.OffsetBox` The box that will be anchored. prop : `.FontProperties` @@ -1054,12 +1059,22 @@ def set_bbox_to_anchor(self, bbox, transform=None): @_compat_get_offset def get_offset(self, bbox, renderer): # docstring inherited - pad = (self.borderpad - * renderer.points_to_pixels(self.prop.get_size_in_points())) + fontsize_in_pixels = renderer.points_to_pixels(self.prop.get_size_in_points()) + try: + borderpad_x, borderpad_y = self.borderpad + except TypeError: + borderpad_x = self.borderpad + borderpad_y = self.borderpad + pad_x_pixels = borderpad_x * fontsize_in_pixels + pad_y_pixels = borderpad_y * fontsize_in_pixels bbox_to_anchor = self.get_bbox_to_anchor() x0, y0 = _get_anchored_bbox( - self.loc, Bbox.from_bounds(0, 0, bbox.width, bbox.height), - bbox_to_anchor, pad) + self.loc, + Bbox.from_bounds(0, 0, bbox.width, bbox.height), + bbox_to_anchor, + pad_x_pixels, + pad_y_pixels + ) return x0 - bbox.x0, y0 - bbox.y0 def update_frame(self, bbox, fontsize=None): @@ -1084,15 +1099,15 @@ def draw(self, renderer): self.stale = False -def _get_anchored_bbox(loc, bbox, parentbbox, borderpad): +def _get_anchored_bbox(loc, bbox, parentbbox, pad_x, pad_y): """ Return the (x, y) position of the *bbox* anchored at the *parentbbox* with - the *loc* code with the *borderpad*. + the *loc* code with the *borderpad* and padding *pad_x*, *pad_y*. """ # This is only called internally and *loc* should already have been # validated. If 0 (None), we just let ``bbox.anchored`` raise. c = [None, "NE", "NW", "SW", "SE", "E", "W", "E", "S", "N", "C"][loc] - container = parentbbox.padded(-borderpad) + container = parentbbox.padded(-pad_x, -pad_y) return bbox.anchored(c, container=container).p0 diff --git a/lib/matplotlib/offsetbox.pyi b/lib/matplotlib/offsetbox.pyi index 8a2016c0320a..36f31908eebf 100644 --- a/lib/matplotlib/offsetbox.pyi +++ b/lib/matplotlib/offsetbox.pyi @@ -157,7 +157,7 @@ class AnchoredOffsetbox(OffsetBox): loc: str, *, pad: float = ..., - borderpad: float = ..., + borderpad: float | tuple[float, float] = ..., child: OffsetBox | None = ..., prop: FontProperties | None = ..., frameon: bool = ..., @@ -185,7 +185,7 @@ class AnchoredText(AnchoredOffsetbox): loc: str, *, pad: float = ..., - borderpad: float = ..., + borderpad: float | tuple[float, float] = ..., prop: dict[str, Any] | None = ..., **kwargs ) -> None: ... diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 477eee9f5a7a..0c8d6b5fee15 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -459,7 +459,8 @@ def set_linewidth(self, w): w : float or None """ w = mpl._val_or_rc(w, 'patch.linewidth') - self._linewidth = float(w) + w = float(w) + self._linewidth = w self._dash_pattern = mlines._scale_dashes(*self._unscaled_dash_pattern, w) self.stale = True @@ -467,26 +468,40 @@ def set_linestyle(self, ls): """ Set the patch linestyle. - ======================================================= ================ - linestyle description - ======================================================= ================ - ``'-'`` or ``'solid'`` solid line - ``'--'`` or ``'dashed'`` dashed line - ``'-.'`` or ``'dashdot'`` dash-dotted line - ``':'`` or ``'dotted'`` dotted line - ``''`` or ``'none'`` (discouraged: ``'None'``, ``' '``) draw nothing - ======================================================= ================ + Parameters + ---------- + ls : {'-', '--', '-.', ':', '', ...} or (offset, on-off-seq) + Possible values: - Alternatively a dash tuple of the following form can be provided:: + - A string: - (offset, onoffseq) + ======================================================= ================ + linestyle description + ======================================================= ================ + ``'-'`` or ``'solid'`` solid line + ``'--'`` or ``'dashed'`` dashed line + ``'-.'`` or ``'dashdot'`` dash-dotted line + ``':'`` or ``'dotted'`` dotted line + ``''`` or ``'none'`` (discouraged: ``'None'``, ``' '``) draw nothing + ======================================================= ================ - where ``onoffseq`` is an even length tuple of on and off ink in points. + - A tuple describing the start position and lengths of dashes and spaces: - Parameters - ---------- - ls : {'-', '--', '-.', ':', '', (offset, on-off-seq), ...} - The line style. + (offset, onoffseq) + + where + + - *offset* is a float specifying the offset (in points); i.e. how much + is the dash pattern shifted. + - *onoffseq* is a sequence of on and off ink in points. There can be + arbitrary many pairs of on and off values. + + Example: The tuple ``(0, (10, 5, 1, 5))`` means that the pattern starts + at the beginning of the line. It draws a 10 point long dash, + then a 5 point long space, then a 1 point long dash, followed by a 5 point + long space, and then the pattern repeats. + + For examples see :doc:`/gallery/lines_bars_and_markers/linestyles`. """ if ls is None: ls = "solid" @@ -791,6 +806,10 @@ def __init__(self, xy, width, height, *, ---------------- **kwargs : `~matplotlib.patches.Patch` properties %(Patch:kwdoc)s + + See Also + -------- + FancyBboxPatch : A rectangle with a fancy box style, e.g. rounded corners. """ super().__init__(**kwargs) self._x0 = xy[0] @@ -2381,10 +2400,9 @@ def pprint_styles(cls): @classmethod @_api.deprecated( - '3.10.0', + '3.10', message="This method is never used internally.", - alternative="No replacement. Please open an issue if you use this." - ) + alternative="No replacement. Please open an issue if you use this.") def register(cls, name, style): """Register a new style.""" if not issubclass(style, cls._Base): diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 00b400e0977b..75e1295f77f1 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -207,7 +207,7 @@ def transform_non_affine(self, values): # docstring inherited x, y = values.T r = np.hypot(x, y) - theta = (np.arctan2(y, x) + 2 * np.pi) % (2 * np.pi) + theta = np.arctan2(y, x) % (2 * np.pi) if self._use_rmin and self._axis is not None: r += self._axis.get_rorigin() r *= self._axis.get_rsign() @@ -431,6 +431,7 @@ class RadialLocator(mticker.Locator): scale of the *r*-axis). """ + @_api.delete_parameter("3.11", "axes") def __init__(self, base, axes=None): self.base = base self._axes = axes @@ -440,11 +441,11 @@ def set_axis(self, axis): def __call__(self): # Ensure previous behaviour with full circle non-annular views. - if self._axes: - if _is_full_circle_rad(*self._axes.viewLim.intervalx): - rorigin = self._axes.get_rorigin() * self._axes.get_rsign() - if self._axes.get_rmin() <= rorigin: - return [tick for tick in self.base() if tick > rorigin] + ax = self.base.axis.axes + if _is_full_circle_rad(*ax.viewLim.intervalx): + rorigin = ax.get_rorigin() * ax.get_rsign() + if ax.get_rmin() <= rorigin: + return [tick for tick in self.base() if tick > rorigin] return self.base() def _zero_in_bounds(self): @@ -452,7 +453,7 @@ def _zero_in_bounds(self): Return True if zero is within the valid values for the scale of the radial axis. """ - vmin, vmax = self._axes.yaxis._scale.limit_range_for_scale(0, 1, 1e-5) + vmin, vmax = self.base.axis._scale.limit_range_for_scale(0, 1, 1e-5) return vmin == 0 def nonsingular(self, vmin, vmax): @@ -468,7 +469,7 @@ def view_limits(self, vmin, vmax): if self._zero_in_bounds() and vmax > vmin: # this allows inverted r/y-lims vmin = min(0, vmin) - return mtransforms.nonsingular(vmin, vmax) + return mtransforms._nonsingular(vmin, vmax) class _ThetaShift(mtransforms.ScaledTranslation): @@ -681,7 +682,7 @@ def __init__(self, *args, **kwargs): def set_major_locator(self, locator): if not isinstance(locator, RadialLocator): - locator = RadialLocator(locator, self.axes) + locator = RadialLocator(locator) super().set_major_locator(locator) def clear(self): diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 1f9c4606af27..61f5f48f1224 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -1,5 +1,9 @@ -# Note: The first part of this file can be modified in place, but the latter -# part is autogenerated by the boilerplate.py script. +# Note: The first part of this file is hand-written and must be edited +# in-place. The second part, starting with +# ### REMAINING CONTENT GENERATED BY boilerplate.py ### +# is generated by the script boilerplate.py. It must not be edited here +# because all changes will be overwritten by the next run of the script. +# For more information see the description in boilerplate.py. """ `matplotlib.pyplot` is a state-based interface to matplotlib. It provides @@ -54,7 +58,6 @@ from cycler import cycler # noqa: F401 import matplotlib -import matplotlib.colorbar import matplotlib.image from matplotlib import _api # Re-exported (import x as x) for typing. @@ -123,13 +126,14 @@ from matplotlib.container import ( BarContainer, ErrorbarContainer, + PieContainer, StemContainer, ) from matplotlib.figure import SubFigure from matplotlib.legend import Legend from matplotlib.mlab import GaussianKDE from matplotlib.image import AxesImage, FigureImage - from matplotlib.patches import FancyArrow, StepPatch, Wedge + from matplotlib.patches import FancyArrow, StepPatch from matplotlib.quiver import Barbs, Quiver, QuiverKey from matplotlib.scale import ScaleBase from matplotlib.typing import ( @@ -143,7 +147,10 @@ MarkerType, MouseEventType, PickEventType, + RcGroupKeyType, + RcKeyType, ResizeEventType, + LogLevel ) from matplotlib.widgets import SubplotTool @@ -351,7 +358,7 @@ def uninstall_repl_displayhook() -> None: # Ensure this appears in the pyplot docs. @_copy_docstring_and_deprecators(matplotlib.set_loglevel) -def set_loglevel(level: str) -> None: +def set_loglevel(level: LogLevel) -> None: return matplotlib.set_loglevel(level) @@ -786,13 +793,13 @@ def pause(interval: float) -> None: @_copy_docstring_and_deprecators(matplotlib.rc) -def rc(group: str, **kwargs) -> None: +def rc(group: RcGroupKeyType, **kwargs) -> None: matplotlib.rc(group, **kwargs) @_copy_docstring_and_deprecators(matplotlib.rc_context) def rc_context( - rc: dict[str, Any] | None = None, + rc: dict[RcKeyType, Any] | None = None, fname: str | pathlib.Path | os.PathLike | None = None, ) -> AbstractContextManager[None]: return matplotlib.rc_context(rc, fname) @@ -932,6 +939,10 @@ def figure( window title is set to this value. If num is a ``SubFigure``, its parent ``Figure`` is activated. + If *num* is a Figure instance that is already tracked in pyplot, it is + activated. If *num* is a Figure instance that is not tracked in pyplot, + it is added to the tracked figures and activated. + figsize : (float, float) or (float, float, str), default: :rc:`figure.figsize` The figure dimensions. This can be @@ -1018,21 +1029,32 @@ def figure( in the matplotlibrc file. """ allnums = get_fignums() + next_num = max(allnums) + 1 if allnums else 1 if isinstance(num, FigureBase): # type narrowed to `Figure | SubFigure` by combination of input and isinstance + has_figure_property_parameters = ( + any(param is not None for param in [figsize, dpi, facecolor, edgecolor]) + or not frameon or kwargs + ) + root_fig = num.get_figure(root=True) if root_fig.canvas.manager is None: - raise ValueError("The passed figure is not managed by pyplot") - elif (any(param is not None for param in [figsize, dpi, facecolor, edgecolor]) - or not frameon or kwargs) and root_fig.canvas.manager.num in allnums: + if has_figure_property_parameters: + raise ValueError( + "You cannot pass figure properties when calling figure() with " + "an existing Figure instance") + backend = _get_backend_mod() + manager_ = backend.new_figure_manager_given_figure(next_num, root_fig) + _pylab_helpers.Gcf._set_new_active_manager(manager_) + return manager_.canvas.figure + elif has_figure_property_parameters and root_fig.canvas.manager.num in allnums: _api.warn_external( "Ignoring specified arguments in this call because figure " f"with num: {root_fig.canvas.manager.num} already exists") _pylab_helpers.Gcf.set_active(root_fig.canvas.manager) return root_fig - next_num = max(allnums) + 1 if allnums else 1 fig_label = '' if num is None: num = next_num @@ -1241,7 +1263,7 @@ def close(fig: None | int | str | Figure | Literal["all"] = None) -> None: ----- pyplot maintains a reference to figures created with `figure()`. When work on the figure is completed, it should be closed, i.e. deregistered - from pyplot, to free its memory (see also :rc:figure.max_open_warning). + from pyplot, to free its memory (see also :rc:`figure.max_open_warning`). Closing a figure window created by `show()` automatically deregisters the figure. For all other use cases, most prominently `savefig()` without `show()`, the figure must be deregistered explicitly using `close()`. @@ -1340,7 +1362,7 @@ def axes( - *None*: A new full window Axes is added using ``subplot(**kwargs)``. - - 4-tuple of floats *rect* = ``(left, bottom, width, height)``. + - 4-tuple of float *rect* = ``(left, bottom, width, height)``. A new Axes is added with dimensions *rect* in normalized (0, 1) units using `~.Figure.add_axes` on the current figure. @@ -1820,7 +1842,7 @@ def subplots( axs[0, 0].plot(x, y) axs[1, 1].scatter(x, y) - # Share a X axis with each column of subplots + # Share an X axis with each column of subplots plt.subplots(2, 2, sharex='col') # Share a Y axis with each row of subplots @@ -3221,12 +3243,17 @@ def boxplot( def broken_barh( xranges: Sequence[tuple[float, float]], yrange: tuple[float, float], + align: Literal["bottom", "center", "top"] = "bottom", *, data=None, **kwargs, ) -> PolyCollection: return gca().broken_barh( - xranges, yrange, **({"data": data} if data is not None else {}), **kwargs + xranges, + yrange, + align=align, + **({"data": data} if data is not None else {}), + **kwargs, ) @@ -3244,8 +3271,9 @@ def cohere( NFFT: int = 256, Fs: float = 2, Fc: int = 0, - detrend: Literal["none", "mean", "linear"] - | Callable[[ArrayLike], ArrayLike] = mlab.detrend_none, + detrend: ( + Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] + ) = mlab.detrend_none, window: Callable[[ArrayLike], ArrayLike] | ArrayLike = mlab.window_hanning, noverlap: int = 0, pad_to: int | None = None, @@ -3302,9 +3330,9 @@ def csd( NFFT: int | None = None, Fs: float | None = None, Fc: int | None = None, - detrend: Literal["none", "mean", "linear"] - | Callable[[ArrayLike], ArrayLike] - | None = None, + detrend: ( + Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] | None + ) = None, window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = None, noverlap: int | None = None, pad_to: int | None = None, @@ -3930,7 +3958,7 @@ def pie( normalize: bool = True, hatch: str | Sequence[str] | None = None, data=None, -) -> tuple[list[Wedge], list[Text]] | tuple[list[Wedge], list[Text], list[Text]]: +) -> PieContainer: return gca().pie( x, explode=explode, @@ -3954,6 +3982,28 @@ def pie( ) +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +@_copy_docstring_and_deprecators(Axes.pie_label) +def pie_label( + container: PieContainer, + /, + labels: str | Sequence[str], + *, + distance: float = 0.6, + textprops: dict | None = None, + rotate: bool = False, + alignment: str = "auto", +) -> list[Text]: + return gca().pie_label( + container, + labels, + distance=distance, + textprops=textprops, + rotate=rotate, + alignment=alignment, + ) + + # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.plot) def plot( @@ -3979,9 +4029,9 @@ def psd( NFFT: int | None = None, Fs: float | None = None, Fc: int | None = None, - detrend: Literal["none", "mean", "linear"] - | Callable[[ArrayLike], ArrayLike] - | None = None, + detrend: ( + Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] | None + ) = None, window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = None, noverlap: int | None = None, pad_to: int | None = None, @@ -4089,9 +4139,9 @@ def specgram( NFFT: int | None = None, Fs: float | None = None, Fc: int | None = None, - detrend: Literal["none", "mean", "linear"] - | Callable[[ArrayLike], ArrayLike] - | None = None, + detrend: ( + Literal["none", "mean", "linear"] | Callable[[ArrayLike], ArrayLike] | None + ) = None, window: Callable[[ArrayLike], ArrayLike] | ArrayLike | None = None, noverlap: int | None = None, cmap: str | Colormap | None = None, @@ -4158,15 +4208,12 @@ def spy( # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.stackplot) -def stackplot( - x, *args, labels=(), colors=None, hatch=None, baseline="zero", data=None, **kwargs -): +def stackplot(x, *args, labels=(), colors=None, baseline="zero", data=None, **kwargs): return gca().stackplot( x, *args, labels=labels, colors=colors, - hatch=hatch, baseline=baseline, **({"data": data} if data is not None else {}), **kwargs, @@ -4408,10 +4455,9 @@ def violinplot( showmedians: bool = False, quantiles: Sequence[float | Sequence[float]] | None = None, points: int = 100, - bw_method: Literal["scott", "silverman"] - | float - | Callable[[GaussianKDE], float] - | None = None, + bw_method: ( + Literal["scott", "silverman"] | float | Callable[[GaussianKDE], float] | None + ) = None, side: Literal["both", "low", "high"] = "both", facecolor: Sequence[ColorType] | ColorType | None = None, linecolor: Sequence[ColorType] | ColorType | None = None, diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index 91c510ca7060..9ffcec5117d9 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -144,7 +144,7 @@ length in y direction = $\\frac{v}{\\mathrm{scale}} \\mathrm{scale_unit}$ - For example, ``(u, v) = (0.5, 0)`` with ``scale=10, scale_unit="width"`` results + For example, ``(u, v) = (0.5, 0)`` with ``scale=10, scale_units="width"`` results in a horizontal arrow with a length of *0.5 / 10 * "width"*, i.e. 0.05 times the Axes width. @@ -490,11 +490,8 @@ class Quiver(mcollections.PolyCollection): """ Specialized PolyCollection for arrows. - The only API method is set_UVC(), which can be used - to change the size, orientation, and color of the - arrows; their locations are fixed when the class is - instantiated. Possibly this method will be useful - in animations. + Use set_UVC to change the size, orientation, and color of the + arrows; their locations can be set using set_offsets(). Much of the work in this class is done in the draw() method so that as much information as possible is available diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 80d25659888e..5cd42750d27f 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -13,12 +13,17 @@ root source directory. """ + import ast +from dataclasses import dataclass from functools import lru_cache, reduce from numbers import Real import operator import os +from pathlib import Path import re +from typing import Any +from collections.abc import Callable import numpy as np @@ -67,6 +72,36 @@ def __call__(self, s): msg += "; remove quotes surrounding your string" raise ValueError(msg) + def __repr__(self): + return (f"{self.__class__.__name__}(" + f"key={self.key!r}, valid={[*self.valid.values()]}, " + f"ignorecase={self.ignorecase})") + + def __eq__(self, other): + if self is other: + return True + if not isinstance(other, ValidateInStrings): + return NotImplemented + return ( + self.key, + self.ignorecase, + self._deprecated_since, + tuple(sorted(self.valid.items())) + ) == ( + other.key, + other.ignorecase, + other._deprecated_since, + tuple(sorted(other.valid.items())) + ) + + def __hash__(self): + return hash(( + self.key, + self.ignorecase, + self._deprecated_since, + tuple(sorted(self.valid.items())) + )) + def _single_string_color_list(s, scalar_validator): """ @@ -215,10 +250,11 @@ def validator(s): validate_string, doc='return a list of strings') validate_int = _make_type_validator(int) validate_int_or_None = _make_type_validator(int, allow_none=True) +validate_intlist = _listify_validator(validate_int, n=2) validate_float = _make_type_validator(float) validate_float_or_None = _make_type_validator(float, allow_none=True) validate_floatlist = _listify_validator( - validate_float, doc='return a list of floats') + validate_float) def _validate_marker(s): @@ -1112,7 +1148,7 @@ def _convert_validator_spec(key, conv): "axes.labelcolor": validate_color, # color of axis label # use scientific notation if log10 of the axis range is smaller than the # first or larger than the second - "axes.formatter.limits": _listify_validator(validate_int, n=2), + "axes.formatter.limits": validate_intlist, # use current locale to format ticks "axes.formatter.use_locale": validate_bool, "axes.formatter.use_mathtext": validate_bool, @@ -1184,6 +1220,8 @@ def _convert_validator_spec(key, conv): "legend.frameon": validate_bool, # alpha value of the legend frame "legend.framealpha": validate_float_or_None, + # linewidth of legend frame + "legend.linewidth": validate_float_or_None, ## the following dimensions are in fraction of the font size "legend.borderpad": validate_float, # units are fontsize @@ -1403,3 +1441,1855 @@ def _convert_validator_spec(key, conv): } _validators = {k: _convert_validator_spec(k, conv) for k, conv in _validators.items()} + + +@dataclass +class _Param: + name: str + default: Any + validator: Callable[[Any], Any] + description: str = None + + +_params = [ + _Param( + "webagg.port", + default=8988, + validator=validate_int, + description="The port to use for the web server in the WebAgg backend." + ), + _Param( + "webagg.address", + default="127.0.0.1", + validator=validate_string, + description="The address on which the WebAgg web server should be reachable." + ), + _Param( + "webagg.port_retries", + default=50, + validator=validate_int, + description="If webagg.port is unavailable, a number of other random ports " + "will be tried until one that is available is found." + ), + _Param( + "webagg.open_in_browser", + default=True, + validator=validate_bool, + description="When True, open the web browser to the plot that is shown" + ), + _Param( + "backend_fallback", + default=True, + validator=validate_bool, + description="If you are running pyplot inside a GUI and your backend choice " + "conflicts, we will automatically try to find a compatible one for " + "you if backend_fallback is True" + ), + _Param( + "interactive", + default=False, + validator=validate_bool + ), + _Param( + "figure.hooks", + default=[], + validator=validate_stringlist, + description="list of dotted.module.name:dotted.callable.name" + ), + _Param( + "toolbar", + default="toolbar2", + validator=_validate_toolbar, + description="{None, toolbar2, toolmanager}" + ), + _Param( + "timezone", + default="UTC", + validator=validate_string, + description="a pytz timezone string, e.g., US/Central or Europe/Paris" + ), + _Param( + "lines.linewidth", + default=1.5, + validator=validate_float, + description="line width in points" + ), + _Param( + "lines.linestyle", + default="-", + validator=_validate_linestyle, + description="solid line" + ), + _Param( + "lines.color", + default="C0", + validator=validate_color, + description="has no affect on plot(); see axes.prop_cycle" + ), + _Param( + "lines.marker", + default="None", + validator=_validate_marker, + description="the default marker" + ), + _Param( + "lines.markerfacecolor", + default="auto", + validator=validate_color_or_auto, + description="the default marker face color" + ), + _Param( + "lines.markeredgecolor", + default="auto", + validator=validate_color_or_auto, + description="the default marker edge color" + ), + _Param( + "lines.markeredgewidth", + default=1.0, + validator=validate_float, + description="the line width around the marker symbol" + ), + _Param( + "lines.markersize", + default=6.0, + validator=validate_float, + description="marker size, in points" + ), + _Param( + "lines.dash_joinstyle", + default="round", + validator=JoinStyle, + description="{miter, round, bevel}" + ), + _Param( + "lines.dash_capstyle", + default="butt", + validator=CapStyle, + description="{butt, round, projecting}" + ), + _Param( + "lines.solid_joinstyle", + default="round", + validator=JoinStyle, + description="{miter, round, bevel}" + ), + _Param( + "lines.solid_capstyle", + default="projecting", + validator=CapStyle, + description="{butt, round, projecting}" + ), + _Param( + "lines.antialiased", + default=True, + validator=validate_bool, + description="render lines in antialiased (no jaggies)" + ), + _Param( + "lines.dashed_pattern", + default=[3.7, 1.6], + validator=validate_floatlist, + description="The dash pattern for linestyle 'dashed'" + ), + _Param( + "lines.dashdot_pattern", + default=[6.4, 1.6, 1.0, 1.6], + validator=validate_floatlist, + description="The dash pattern for linestyle 'dashdot'" + ), + _Param( + "lines.dotted_pattern", + default=[1.0, 1.65], + validator=validate_floatlist, + description="The dash pattern for linestyle 'dotted'" + ), + _Param( + "lines.scale_dashes", + default=True, + validator=validate_bool + ), + _Param( + "markers.fillstyle", + default="full", + validator=validate_fillstyle, + description="{full, left, right, bottom, top, none}" + ), + _Param( + "pcolor.shading", + default="auto", + validator=["auto", "flat", "nearest", "gouraud"] + ), + _Param( + "pcolormesh.snap", + default=True, + validator=validate_bool, + description="Whether to snap the mesh to pixel boundaries. This is provided " + "solely to allow old test images to remain unchanged. Set to False " + "to obtain the previous behavior." + ), + _Param( + "patch.linewidth", + default=1.0, + validator=validate_float, + description="edge width in points." + ), + _Param( + "patch.facecolor", + default="C0", + validator=validate_color + ), + _Param( + "patch.edgecolor", + default="black", + validator=validate_color, + description='By default, Patches and Collections do not draw edges. This value ' + 'is only used if facecolor is "none" (an Artist without facecolor ' + 'and edgecolor would be invisible) or if patch.force_edgecolor ' + 'is True.' + ), + _Param( + "patch.force_edgecolor", + default=False, + validator=validate_bool, + description="By default, Patches and Collections do not draw edges. Set this " + "to True to draw edges with patch.edgedcolor as the default " + "edgecolor. This is mainly relevant for styles." + ), + _Param( + "patch.antialiased", + default=True, + validator=validate_bool, + description="render patches in antialiased (no jaggies)" + ), + _Param("hatch.color", "edge", _validate_color_or_edge), + _Param("hatch.linewidth", 1.0, validate_float), + _Param("boxplot.notch", False, validate_bool), + _Param("boxplot.vertical", True, validate_bool), + _Param("boxplot.whiskers", 1.5, validate_whiskers), + _Param("boxplot.bootstrap", None, validate_int_or_None), + _Param("boxplot.patchartist", False, validate_bool), + _Param("boxplot.showmeans", False, validate_bool), + _Param("boxplot.showcaps", True, validate_bool), + _Param("boxplot.showbox", True, validate_bool), + _Param("boxplot.showfliers", True, validate_bool), + _Param("boxplot.meanline", False, validate_bool), + _Param("boxplot.flierprops.color", "black", validate_color), + _Param("boxplot.flierprops.marker", "o", _validate_marker), + _Param("boxplot.flierprops.markerfacecolor", "none", validate_color_or_auto), + _Param("boxplot.flierprops.markeredgecolor", "black", validate_color), + _Param("boxplot.flierprops.markeredgewidth", 1.0, validate_float), + _Param("boxplot.flierprops.markersize", 6.0, validate_float), + _Param("boxplot.flierprops.linestyle", "none", _validate_linestyle), + _Param("boxplot.flierprops.linewidth", 1.0, validate_float), + _Param("boxplot.boxprops.color", "black", validate_color), + _Param("boxplot.boxprops.linewidth", 1.0, validate_float), + _Param("boxplot.boxprops.linestyle", "-", _validate_linestyle), + _Param("boxplot.whiskerprops.color", "black", validate_color), + _Param("boxplot.whiskerprops.linewidth", 1.0, validate_float), + _Param("boxplot.whiskerprops.linestyle", "-", _validate_linestyle), + _Param("boxplot.capprops.color", "black", validate_color), + _Param("boxplot.capprops.linewidth", 1.0, validate_float), + _Param("boxplot.capprops.linestyle", "-", _validate_linestyle), + _Param("boxplot.medianprops.color", "C1", validate_color), + _Param("boxplot.medianprops.linewidth", 1.0, validate_float), + _Param("boxplot.medianprops.linestyle", "-", _validate_linestyle), + _Param("boxplot.meanprops.color", "C2", validate_color), + _Param("boxplot.meanprops.marker", "^", _validate_marker), + _Param("boxplot.meanprops.markerfacecolor", "C2", validate_color), + _Param("boxplot.meanprops.markeredgecolor", "C2", validate_color), + _Param("boxplot.meanprops.markersize", 6.0, validate_float), + _Param("boxplot.meanprops.linestyle", "--", _validate_linestyle), + _Param("boxplot.meanprops.linewidth", 1.0, validate_float), + _Param("font.family", ["sans-serif"], validate_stringlist), + _Param("font.style", "normal", validate_string), + _Param("font.variant", "normal", validate_string), + _Param("font.weight", "normal", validate_fontweight), + _Param("font.stretch", "normal", validate_fontstretch), + _Param("font.size", 10.0, validate_float), + _Param( + "font.serif", + default=[ + "DejaVu Serif", "Bitstream Vera Serif", "Computer Modern Roman", + "New Century Schoolbook", "Century Schoolbook L", "Utopia", "ITC Bookman", + "Bookman", "Nimbus Roman No9 L", "Times New Roman", "Times", "Palatino", + "Charter", "serif", + ], + validator=validate_stringlist + ), + _Param( + "font.sans-serif", + default=[ + "DejaVu Sans", "Bitstream Vera Sans", "Computer Modern Sans Serif", + "Lucida Grande", "Verdana", "Geneva", "Lucid", "Arial", "Helvetica", + "Avant Garde", "sans-serif", + ], + validator=validate_stringlist + ), + _Param( + "font.cursive", + default=[ + "Apple Chancery", "Textile", "Zapf Chancery", "Sand", "Script MT", "Felipa", + "Comic Neue", "Comic Sans MS", "cursive", + ], + validator=validate_stringlist + ), + _Param( + "font.fantasy", + default=["Chicago", "Charcoal", "Impact", "Western", "xkcd script", "fantasy"], + validator=validate_stringlist + ), + _Param( + "font.monospace", + default=[ + "DejaVu Sans Mono", "Bitstream Vera Sans Mono", + "Computer Modern Typewriter", "Andale Mono", "Nimbus Mono L", "Courier New", + "Courier", "Fixed", "Terminal", "monospace", + ], + validator=validate_stringlist + ), + _Param( + "font.enable_last_resort", + default=True, + validator=validate_bool, + description="If True, then Unicode Consortium's Last Resort font will be " + "appended to all font selections. This ensures that there will " + "always be a glyph displayed." + ), + _Param( + "text.color", + default="black", + validator=validate_color + ), + _Param("text.hinting", + default="force_autohint", + validator=[ + "default", "no_autohint", "force_autohint", "no_hinting", "auto", "native", + "either", "none", + ], + description="FreeType hinting flag (\"foo\" corresponds to FT_LOAD_FOO); may " + "be one of the following (Proprietary Matplotlib-specific synonyms " + "are given in parentheses, but their use is discouraged): " + "- default: Use the font's native hinter if possible, else " + " FreeType's auto-hinter. (\"either\" is a synonym)." + "- no_autohint: Use the font's native hinter if possible, else " + " don't hint. (\"native\" is a synonym.)" + "- force_autohint: Use FreeType's auto-hinter. (\"auto\" is a " + " synonym.)" + "- no_hinting: Disable hinting. (\"none\" is a synonym.)" + ), + _Param( + "text.hinting_factor", + default=8, + validator=validate_int, + description="Specifies the amount of softness for hinting in the horizontal " + "direction. A value of 1 will hint to full pixels. A value of 2 " + "will hint to half pixels etc." + ), + _Param( + "text.kerning_factor", + default=0, + validator=validate_int, + description="Specifies the scaling factor for kerning values. This is " + "provided solely to allow old test images to remain unchanged. " + "Set to 6 to obtain previous behavior. Values other than 0 or 6 " + "have no defined meaning." + ), + _Param( + "text.antialiased", + default=True, + validator=validate_bool, + description="If True (default), the text will be antialiased. This only " + "affects raster outputs." + ), + _Param( + "text.parse_math", + default=True, + validator=validate_bool, + description="Use mathtext if there is an even number of unescaped dollar signs." + + ), + _Param( + "text.usetex", + default=False, + validator=validate_bool, + description="use latex for all text handling. The following fonts are " + "supported through the usual rc parameter settings: " + "new century schoolbook, bookman, times, palatino, zapf chancery, " + "charter, serif, sans-serif, helvetica, avant garde, courier, " + "monospace, computer modern roman, computer modern sans serif, " + "computer modern typewriter" + ), + _Param( + "text.latex.preamble", + default="", + validator=validate_string, + description='IMPROPER USE OF THIS FEATURE WILL LEAD TO LATEX FAILURES AND IS ' + 'THEREFORE UNSUPPORTED. PLEASE DO NOT ASK FOR HELP IF THIS FEATURE ' + 'DOES NOT DO WHAT YOU EXPECT IT TO. text.latex.preamble is a ' + 'single line of LaTeX code that will be passed on to the LaTeX ' + 'system. It may contain any code that is valid for the LaTeX ' + '"preamble", i.e. between the "\\documentclass" and ' + '"\\begin{document}" statements. Note that it has to be put on a ' + 'single line, which may become quite long. The following packages ' + 'are always loaded with usetex, so beware of package collisions: ' + ' color, fix-cm, geometry, graphicx, textcomp. PostScript ' + '(PSNFSS) font packages may also be loaded, depending on your font ' + 'settings.' + ), + _Param( + "mathtext.fontset", + default="dejavusans", + validator=["dejavusans", "dejavuserif", "cm", "stix", "stixsans", "custom"], + description="Should be 'dejavusans' (default), 'dejavuserif', " + "'cm' (Computer Modern), 'stix', 'stixsans' or 'custom'" + ), + _Param("mathtext.bf", "sans:bold", validate_font_properties), + _Param("mathtext.bfit", "sans:italic:bold", validate_font_properties), + _Param("mathtext.cal", "cursive", validate_font_properties), + _Param("mathtext.it", "sans:italic", validate_font_properties), + _Param("mathtext.rm", "sans", validate_font_properties), + _Param("mathtext.sf", "sans", validate_font_properties), + _Param("mathtext.tt", "monospace", validate_font_properties), + _Param( + "mathtext.fallback", + default="cm", + validator=_validate_mathtext_fallback, + description="Select fallback font from ['cm' (Computer Modern), 'stix', " + "'stixsans'] when a symbol cannot be found in one of the custom " + "math fonts. Select 'None' to not perform fallback and replace the " + "missing character by a dummy symbol." + ), + _Param("mathtext.default", "it", + ["rm", "cal", "bfit", "it", "tt", "sf", "bf", "default", "bb", "frak", "scr", + "regular", ], + description='The default font to use for math. Can be any of the LaTeX font ' + 'names, including the special name "regular" for the same font ' + 'used in regular text.', + ), + _Param( + "axes.facecolor", + default="white", + validator=validate_color, + description="axes background color" + ), + _Param( + "axes.edgecolor", + default="black", + validator=validate_color, + description="axes edge color" + ), + _Param( + "axes.linewidth", + default=0.8, + validator=validate_float, + description="edge line width" + ), + _Param( + "axes.grid", + default=False, + validator=validate_bool, + description="display grid or not" + ), + _Param( + "axes.grid.axis", + default="both", + validator=["x", "y", "both"], + description="which axis the grid should apply to" + ), + _Param( + "axes.grid.which", + default="major", + validator=["minor", "both", "major"], + description="grid lines at {major, minor, both} ticks" + ), + _Param( + "axes.titlelocation", + default="center", + validator=["left", "center", "right"], + description="alignment of the title: {left, right, center}" + ), + _Param( + "axes.titlesize", + default="large", + validator=validate_fontsize, + description="font size of the axes title" + ), + _Param( + "axes.titleweight", + default="normal", + validator=validate_fontweight, + description="font weight of title" + ), + _Param( + "axes.titlecolor", + default="auto", + validator=validate_color_or_auto, + description="color of the axes title, auto falls back to text.color as default " + "value" + ), + _Param( + "axes.titley", + default=None, + validator=validate_float_or_None, + description="position title (axes relative units). None implies auto" + ), + _Param( + "axes.titlepad", + default=6.0, + validator=validate_float, + description="pad between axes and title in points" + ), + _Param( + "axes.labelsize", + default="medium", + validator=validate_fontsize, + description="font size of the x and y labels" + ), + _Param( + "axes.labelpad", + default=4.0, + validator=validate_float, + description="space between label and axis" + ), + _Param( + "axes.labelweight", + default="normal", + validator=validate_fontweight, + description="weight of the x and y labels" + ), + _Param( + "axes.labelcolor", + default="black", + validator=validate_color + ), + _Param( + "axes.axisbelow", + default="line", + validator=validate_axisbelow, + description="draw axis gridlines and ticks: " + "- below patches (True) " + "- above patches but below lines ('line') " + "- above all (False)" + ), + _Param( + "axes.formatter.limits", + default=[-5, 6], + validator=validate_intlist, + description="use scientific notation if log10 of the axis range is smaller " + "than the first or larger than the second" + ), + _Param( + "axes.formatter.use_locale", + default=False, + validator=validate_bool, + description="When True, format tick labels according to the user's locale. " + "For example, use ',' as a decimal separator in the fr_FR locale." + ), + _Param( + "axes.formatter.use_mathtext", + default=False, + validator=validate_bool, + description="When True, use mathtext for scientific notation." + ), + _Param( + "axes.formatter.min_exponent", + default=0, + validator=validate_int, + description="minimum exponent to format in scientific notation" + ), + _Param( + "axes.formatter.useoffset", + default=True, + validator=validate_bool, + description="If True, the tick label formatter will default to labeling ticks " + "relative to an offset when the data range is small compared to " + "the minimum absolute value of the data." + ), + _Param( + "axes.formatter.offset_threshold", + default=4, + validator=validate_int, + description="When useoffset is True, the offset will be used when it can " + "remove at least this number of significant digits from tick " + "labels." + ), + _Param( + "axes.spines.left", + default=True, + validator=validate_bool, + description="display axis spines" + ), + _Param("axes.spines.bottom", True, validate_bool), + _Param("axes.spines.top", True, validate_bool), + _Param( + "axes.spines.right", + default=True, + validator=validate_bool + ), + _Param( + "axes.unicode_minus", + default=True, + validator=validate_bool, + description="use Unicode for the minus symbol rather than hyphen. See " + "https://en.wikipedia.org/wiki/Plus_and_minus_signs#Character_codes" + + ), + _Param("axes.prop_cycle", + default=cycler( + "color", + [(0.12156862745098039, 0.4666666666666667, 0.7058823529411765), + (1.0, 0.4980392156862745, 0.054901960784313725), + (0.17254901960784313, 0.6274509803921569, 0.17254901960784313), + (0.8392156862745098, 0.15294117647058825, 0.1568627450980392), + (0.5803921568627451, 0.403921568627451, 0.7411764705882353), + (0.5490196078431373, 0.33725490196078434, 0.29411764705882354), + (0.8901960784313725, 0.4666666666666667, 0.7607843137254902), + (0.4980392156862745, 0.4980392156862745, 0.4980392156862745), + (0.7372549019607844, 0.7411764705882353, 0.13333333333333333), + (0.09019607843137255, 0.7450980392156863, 0.8117647058823529), + ], + ), + validator=validate_cycler + ), + _Param( + "axes.xmargin", + default=0.05, + validator=_validate_greaterthan_minushalf, + description="x margin. See `~.axes.Axes.margins`" + ), + _Param( + "axes.ymargin", + default=0.05, + validator=_validate_greaterthan_minushalf, + description="y margin. See `~.axes.Axes.margins`" + ), + _Param( + "axes.zmargin", + default=0.05, + validator=_validate_greaterthan_minushalf, + description="z margin. See `~.axes.Axes.margins`" + ), + _Param( + "axes.autolimit_mode", + default="data", + validator=["data", "round_numbers"], + description='If "data", use axes.xmargin and axes.ymargin as is. If ' + '"round_numbers", after application of margins, axis limits are ' + 'further expanded to the nearest "round" number.', + ), + _Param( + "polaraxes.grid", + default=True, + validator=validate_bool, + description="display grid on polar axes" + ), + _Param( + "axes3d.grid", + default=True, + validator=validate_bool, description="display grid on 3D axes" + ), + _Param( + "axes3d.automargin", + default=False, + validator=validate_bool, + description="automatically add margin when manually setting 3D axis limits" + ), + _Param( + "axes3d.xaxis.panecolor", + default=(0.95, 0.95, 0.95, 0.5), + validator=validate_color, + description="background pane on 3D axes" + ), + _Param( + "axes3d.yaxis.panecolor", + default=(0.9, 0.9, 0.9, 0.5), + validator=validate_color, + description="background pane on 3D axes" + ), + _Param( + "axes3d.zaxis.panecolor", + default=(0.925, 0.925, 0.925, 0.5), + validator=validate_color, + description="background pane on 3D axes" + ), + _Param( + "axes3d.depthshade", + default=True, + validator=validate_bool, + description="depth shade for 3D scatter plots" + ), + _Param( + "axes3d.depthshade_minalpha", + default=0.3, + validator=validate_float, + description="minimum alpha value for depth shading" + ), + _Param( + "axes3d.mouserotationstyle", + default="arcball", + validator=["azel", "trackball", "sphere", "arcball"], + description="{azel, trackball, sphere, arcball} See also " + "https://matplotlib.org/stable/api/toolkits/mplot3d/view_angles.html#rotation-with-mouse"), # noqa + _Param( + "axes3d.trackballsize", + default=0.667, + validator=validate_float, + description="trackball diameter, in units of the Axes bbox" + ), + _Param( + "axes3d.trackballborder", + default=0.2, + validator=validate_float, + description="trackball border width, in units of the Axes bbox (only for " + "'sphere' and 'arcball' style)" + ), + _Param( + "xaxis.labellocation", + default="center", + validator=["left", "center", "right"], + description="alignment of the xaxis label: {left, right, center}" + ), + _Param( + "yaxis.labellocation", + default="center", + validator=["bottom", "center", "top"], + description="alignment of the yaxis label: {bottom, top, center}" + ), + _Param("date.autoformatter.year", "%Y", validate_string), + _Param("date.autoformatter.month", "%Y-%m", validate_string), + _Param("date.autoformatter.day", "%Y-%m-%d", validate_string), + _Param("date.autoformatter.hour", "%m-%d %H", validate_string), + _Param("date.autoformatter.minute", "%d %H:%M", validate_string), + _Param("date.autoformatter.second", "%H:%M:%S", validate_string), + _Param("date.autoformatter.microsecond", "%M:%S.%f", validate_string), + _Param( + "date.epoch", + default="1970-01-01T00:00:00", + validator=_validate_date, + description="The reference date for Matplotlib's internal date representation. " + "See https://matplotlib.org/stable/gallery/ticks/date_precision_and_epochs.html"), #noqa + _Param( + "date.converter", + default="auto", + validator=["auto", "concise"], + description="'auto', 'concise'" + ), + _Param( + "date.interval_multiples", + default=True, + validator=validate_bool, + description="For auto converter whether to use interval_multiples" + ), + _Param( + "xtick.top", + default=False, + validator=validate_bool, + description="draw ticks on the top side" + ), + _Param( + "xtick.bottom", + default=True, + validator=validate_bool, + description="draw ticks on the bottom side" + ), + _Param( + "xtick.labeltop", + default=False, + validator=validate_bool, + description="draw label on the top" + ), + _Param( + "xtick.labelbottom", + default=True, + validator=validate_bool, + description="draw label on the bottom" + ), + _Param( + "xtick.major.size", + default=3.5, + validator=validate_float, + description="major tick size in points" + ), + _Param( + "xtick.minor.size", + default=2.0, + validator=validate_float, + description="minor tick size in points" + ), + _Param( + "xtick.major.width", + default=0.8, + validator=validate_float, + description="major tick width in points" + ), + _Param( + "xtick.minor.width", + default=0.6, + validator=validate_float, + description="minor tick width in points" + ), + _Param( + "xtick.major.pad", + default=3.5, + validator=validate_float, + description="distance to major tick label in points" + ), + _Param( + "xtick.minor.pad", + default=3.4, + validator=validate_float, + description="distance to the minor tick label in points" + ), + _Param( + "xtick.color", + default="black", + validator=validate_color, + description="color of the ticks" + ), + _Param( + "xtick.labelcolor", + default="inherit", + validator=validate_color_or_inherit, + description="color of the tick labels or inherit from xtick.color" + ), + _Param( + "xtick.labelsize", + default="medium", + validator=validate_fontsize, + description="font size of the tick labels" + ), + _Param( + "xtick.direction", + default="out", + validator=["out", "in", "inout"], + description="direction: {in, out, inout}" + ), + _Param( + "xtick.minor.visible", + default=False, + validator=validate_bool, + description="visibility of minor ticks on x-axis" + ), + _Param( + "xtick.major.top", + default=True, + validator=validate_bool, + description="draw x axis top major ticks" + ), + _Param( + "xtick.major.bottom", + default=True, + validator=validate_bool, + description="draw x axis bottom major ticks" + ), + _Param( + "xtick.minor.top", + default=True, + validator=validate_bool, + description="draw x axis top minor ticks" + ), + _Param( + "xtick.minor.bottom", + default=True, + validator=validate_bool, + description="draw x axis bottom minor ticks" + ), + _Param( + "xtick.minor.ndivs", + default="auto", + validator=_validate_minor_tick_ndivs, + description="number of minor ticks between the major ticks on x-axis" + ), + _Param( + "xtick.alignment", + default="center", + validator=["center", "right", "left"], + description="alignment of xticks" + ), + _Param( + "ytick.left", + default=True, + validator=validate_bool, + description="draw ticks on the left side" + ), + _Param( + "ytick.right", + default=False, + validator=validate_bool, + description="draw ticks on the right side" + ), + _Param( + "ytick.labelleft", + default=True, + validator=validate_bool, + description="draw tick labels on the left side" + ), + _Param( + "ytick.labelright", + default=False, + validator=validate_bool, + description="draw tick labels on the right side" + ), + _Param( + "ytick.major.size", + default=3.5, + validator=validate_float, + description="major tick size in points" + ), + _Param( + "ytick.minor.size", + default=2.0, + validator=validate_float, + description="minor tick size in points" + ), + _Param( + "ytick.major.width", + default=0.8, + validator=validate_float, + description="major tick width in points" + ), + _Param( + "ytick.minor.width", + default=0.6, + validator=validate_float, + description="minor tick width in points" + ), + _Param( + "ytick.major.pad", + default=3.5, + validator=validate_float, + description="distance to major tick label in points" + ), + _Param( + "ytick.minor.pad", + default=3.4, + validator=validate_float, + description="distance to the minor tick label in points" + ), + _Param( + "ytick.color", + default="black", + validator=validate_color, + description="color of the ticks" + ), + _Param( + "ytick.labelcolor", + default="inherit", + validator=validate_color_or_inherit, + description="color of the tick labels or inherit from ytick.color" + ), + _Param( + "ytick.labelsize", + default="medium", + validator=validate_fontsize, + description="font size of the tick labels" + ), + _Param( + "ytick.direction", + default="out", + validator=["out", "in", "inout"], + description="direction: {in, out, inout}" + ), + _Param( + "ytick.minor.visible", + default=False, + validator=validate_bool, + description="visibility of minor ticks on y-axis" + ), + _Param( + "ytick.major.left", + default=True, + validator=validate_bool, + description="draw y axis left major ticks" + ), + _Param( + "ytick.major.right", + default=True, + validator=validate_bool, + description="draw y axis right major ticks" + ), + _Param( + "ytick.minor.left", + default=True, + validator=validate_bool, + description="draw y axis left minor ticks" + ), + _Param( + "ytick.minor.right", + default=True, + validator=validate_bool, + description="draw y axis right minor ticks" + ), + _Param( + "ytick.minor.ndivs", + default="auto", + validator=_validate_minor_tick_ndivs, + description="number of minor ticks between the major ticks on y-axis" + ), + _Param("ytick.alignment", "center_baseline", + ["center", "top", "bottom", "baseline", "center_baseline"], + description="alignment of yticks" + ), + _Param( + "grid.color", + default="#b0b0b0", + validator=validate_color, + description='b0b0b0" # grid color' + ), + _Param( + "grid.linestyle", + default="-", + validator=_validate_linestyle, + description="solid" + ), + _Param( + "grid.linewidth", + default=0.8, + validator=validate_float, + description="in points" + ), + _Param( + "grid.alpha", + default=1.0, + validator=validate_float, + description="transparency, between 0.0 and 1.0" + ), + _Param( + "grid.major.color", + default=None, + validator=_validate_color_or_None, + description="If None defaults to grid.color" + ), + _Param( + "grid.major.linestyle", + default=None, + validator=_validate_linestyle_or_None, + description="If None defaults to grid.linestyle" + ), + _Param( + "grid.major.linewidth", + default=None, + validator=validate_float_or_None, + description="If None defaults to grid.linewidth" + ), + _Param( + "grid.major.alpha", + default=None, + validator=validate_float_or_None, + description="If None defaults to grid.alpha" + ), + _Param( + "grid.minor.color", + default=None, + validator=_validate_color_or_None, + description="If None defaults to grid.color" + ), + _Param( + "grid.minor.linestyle", + default=None, + validator=_validate_linestyle_or_None, + description="If None defaults to grid.linestyle" + ), + _Param( + "grid.minor.linewidth", + default=None, + validator=validate_float_or_None, + description="If None defaults to grid.linewidth" + ), + _Param( + "grid.minor.alpha", + default=None, + validator=validate_float_or_None, + description="If None defaults to grid.alpha" + ), + _Param( + "legend.loc", + default="best", + validator=_validate_legend_loc + ), + _Param( + "legend.frameon", + default=True, + validator=validate_bool, + description="if True, draw the legend on a background patch" + ), + _Param( + "legend.framealpha", + default=0.8, + validator=validate_float_or_None, + description="legend patch transparency" + ), + _Param( + "legend.facecolor", + default="inherit", + validator=validate_color_or_inherit, + description="inherit from axes.facecolor; or color spec" + ), + _Param( + "legend.edgecolor", + default="0.8", + validator=validate_color_or_inherit, + description="background patch boundary color" + ), + _Param( + "legend.linewidth", + default=None, + validator=validate_float_or_None, + description="line width of the legend frame, None means inherit from " + "patch.linewidth" + ), + _Param( + "legend.fancybox", + default=True, + validator=validate_bool, + description="if True, use a rounded box for the legend background, else a " + "rectangle" + ), + _Param( + "legend.shadow", + default=False, + validator=validate_bool, + description="if True, give background a shadow effect" + ), + _Param( + "legend.numpoints", + default=1, + validator=validate_int, + description="the number of marker points in the legend line" + ), + _Param( + "legend.scatterpoints", + default=1, + validator=validate_int, + description="number of scatter points" + ), + _Param( + "legend.markerscale", + default=1.0, + validator=validate_float, + description="the relative size of legend markers vs. original" + ), + _Param( + "legend.fontsize", + default="medium", + validator=validate_fontsize + ), + _Param( + "legend.labelcolor", + default="None", + validator=_validate_color_or_linecolor + ), + _Param( + "legend.title_fontsize", + default=None, + validator=validate_fontsize_None, + description="None sets to the same as the default axes." + ), + _Param( + "legend.borderpad", + default=0.4, + validator=validate_float, + description="border whitespace" + ), + _Param( + "legend.labelspacing", + default=0.5, + validator=validate_float, + description="the vertical space between the legend entries" + ), + _Param( + "legend.handlelength", + default=2.0, + validator=validate_float, + description="the length of the legend lines" + ), + _Param( + "legend.handleheight", + default=0.7, + validator=validate_float, + description="the height of the legend handle" + ), + _Param( + "legend.handletextpad", + default=0.8, + validator=validate_float, + description="the space between the legend line and legend text" + ), + _Param( + "legend.borderaxespad", + default=0.5, + validator=validate_float, + description="the border between the axes and legend edge" + ), + _Param( + "legend.columnspacing", + default=2.0, + validator=validate_float, description="column separation" + ), + _Param( + "figure.titlesize", + default="large", + validator=validate_fontsize, + description="size of the figure title (``Figure.suptitle()``)" + ), + _Param( + "figure.titleweight", + default="normal", + validator=validate_fontweight, + description="weight of the figure title" + ), + _Param( + "figure.labelsize", + default="large", + validator=validate_fontsize, + description="size of the figure label (``Figure.sup[x|y]label()``)" + ), + _Param( + "figure.labelweight", + default="normal", + validator=validate_fontweight, + description="weight of the figure label" + ), + _Param( + "figure.figsize", + default=[6.4, 4.8], + validator=_listify_validator(validate_float, n=2), + description="figure size in inches" + ), + _Param( + "figure.dpi", + default=100.0, + validator=validate_float, description="figure dots per inch" + ), + _Param( + "figure.facecolor", + default="white", + validator=validate_color, description="figure face color" + ), + _Param( + "figure.edgecolor", + default="white", + validator=validate_color, description="figure edge color" + ), + _Param( + "figure.frameon", + default=True, + validator=validate_bool, description="enable figure frame" + ), + _Param( + "figure.max_open_warning", + default=20, + validator=validate_int, + description="The maximum number of figures to open through the pyplot " + "interface before emitting a warning. If less than one this " + "feature is disabled." + ), + _Param( + "figure.raise_window", + default=True, + validator=validate_bool, + description="Raise the GUI window to front when show() is called. If set to " + "False, we currently do not take any further actions and whether " + "the window appears on the front may depend on the GUI framework " + "and window manager." + ), + _Param( + "figure.subplot.left", + default=0.125, + validator=validate_float, + description="the left side of the subplots of the figure" + ), + _Param( + "figure.subplot.right", + default=0.9, + validator=validate_float, + description="the right side of the subplots of the figure" + ), + _Param( + "figure.subplot.bottom", + default=0.11, + validator=validate_float, + description="the bottom of the subplots of the figure" + ), + _Param( + "figure.subplot.top", + default=0.88, + validator=validate_float, + description="the top of the subplots of the figure" + ), + _Param( + "figure.subplot.wspace", + default=0.2, + validator=validate_float, + description="the amount of width reserved for space between subplots, " + "expressed as a fraction of the average axis width" + ), + _Param( + "figure.subplot.hspace", + default=0.2, + validator=validate_float, + description="the amount of height reserved for space between subplots, " + "expressed as a fraction of the average axis height" + ), + _Param( + "figure.autolayout", + default=False, + validator=validate_bool, + description="When True, automatically adjust subplot parameters to make the " + "plot fit the figure using `~.Figure.tight_layout`" + ), + _Param( + "figure.constrained_layout.use", + default=False, + validator=validate_bool, + description="When True, automatically make plot elements fit on the figure. " + '(Not compatible with "figure.autolayout", above).' + ), + _Param( + "figure.constrained_layout.h_pad", + default=0.04167, + validator=validate_float, + description="Padding (in inches) around axes; defaults to 3/72 inches, " + "i.e. 3 points" + ), + _Param( + "figure.constrained_layout.w_pad", + default=0.04167, + validator=validate_float, + description="Padding (in inches) around axes; defaults to 3/72 inches, " + "i.e. 3 points" + ), + _Param( + "figure.constrained_layout.hspace", + default=0.02, + validator=validate_float, + description="Spacing between subplots, relative to the subplot sizes. Much " + "smaller than for tight_layout (figure.subplot.hspace, " + "figure.subplot.wspace) as constrained_layout already takes " + "surrounding texts (titles, labels, # ticklabels) into account." + ), + _Param( + "figure.constrained_layout.wspace", + default=0.02, + validator=validate_float, + description="Spacing between subplots, relative to the subplot sizes. Much " + "smaller than for tight_layout (figure.subplot.hspace, " + "figure.subplot.wspace) as constrained_layout already takes " + "surrounding texts (titles, labels, # ticklabels) into account." + ), + _Param( + "image.aspect", + default="equal", + validator=validate_aspect, + description="{equal, auto} or a number" + ), + _Param( + "image.interpolation", + default="auto", + validator=validate_string, + description="see help(imshow) for options" + ), + _Param( + "image.interpolation_stage", + default="auto", + validator=["auto", "data", "rgba"], + description="see help(imshow) for options" + ), + _Param( + "image.cmap", + default="viridis", + validator=_validate_cmap, + description="A colormap name (plasma, magma, etc.)" + ), + _Param( + "image.lut", + default=256, + validator=validate_int, + description="the size of the colormap lookup table" + ), + _Param( + "image.origin", + default="upper", + validator=["upper", "lower"], description="{lower, upper}" + ), + _Param( + "image.resample", + default=True, + validator=validate_bool + ), + _Param( + "image.composite_image", + default=True, + validator=validate_bool, + description="When True, all the images on a set of axes are combined into a " + "single composite image before saving a figure as a vector " + "graphics file, such as a PDF." + ), + _Param( + "contour.negative_linestyle", + default="dashed", + validator=_validate_linestyle, + description="string or on-off ink sequence" + ), + _Param( + "contour.corner_mask", + default=True, + validator=validate_bool, description="{True, False}" + ), + _Param( + "contour.linewidth", + default=None, + validator=validate_float_or_None, + description="{float, None} Size of the contour line widths. If set to None, it " + 'falls back to "line.linewidth".' + ), + _Param( + "contour.algorithm", + default="mpl2014", + validator=["mpl2005", "mpl2014", "serial", "threaded"], + description="{mpl2005, mpl2014, serial, threaded}" + ), + _Param( + "errorbar.capsize", + default=0.0, + validator=validate_float, + description="length of end cap on error bars in pixels" + ), + _Param( + "hist.bins", + default=10, + validator=validate_hist_bins, + description="The default number of histogram bins or 'auto'." + ), + _Param( + "scatter.marker", + default="o", + validator=_validate_marker, + description="The default marker type for scatter plots." + ), + _Param( + "scatter.edgecolors", + default="face", + validator=validate_string, + description="The default edge colors for scatter plots." + ), + _Param( + "agg.path.chunksize", + default=0, + validator=validate_int, + description="0 to disable; values in the range 10000 to 100000 can improve " + "speed slightly and prevent an Agg rendering failure when plotting " + "very large data sets, especially if they are very gappy. It may " + "cause minor artifacts, though. A value of 20000 is probably a " + "good starting point." + ), + _Param( + "path.simplify", + default=True, + validator=validate_bool, + description='When True, simplify paths by removing "invisible" points to ' + 'reduce file size and increase rendering speed', + ), + _Param( + "path.simplify_threshold", + default=0.111111111111, + validator=_validate_greaterequal0_lessequal1, + description="The threshold of similarity below which vertices will be removed " + "in the simplification process." + ), + _Param( + "path.snap", + default=True, + validator=validate_bool, + description="When True, rectilinear axis-aligned paths will be snapped to the " + "nearest pixel when certain criteria are met. When False, paths " + "will never be snapped." + ), + _Param( + "path.sketch", + default=None, + validator=validate_sketch, + description="May be None, or a tuple of the form:" + "path.sketch: (scale, length, randomness)" + "- *scale* is the amplitude of the wiggle perpendicular to the line" + " (in pixels)." + "- *length* is the length of the wiggle along the line (in pixels)." + "- *randomness* is the factor by which the length is randomly " + " scaled." + ), + _Param( + "path.effects", + default=[], + validator=validate_anylist + ), + _Param( + "savefig.dpi", + default="figure", + validator=validate_dpi, + description="figure dots per inch or 'figure'" + ), + _Param( + "savefig.facecolor", + default="auto", + validator=validate_color_or_auto, + description="figure face color when saving" + ), + _Param( + "savefig.edgecolor", + default="auto", + validator=validate_color_or_auto, + description="figure edge color when saving" + ), + _Param( + "savefig.format", + default="png", + validator=validate_string, description="{png, ps, pdf, svg}" + ), + _Param( + "savefig.bbox", + default=None, + validator=validate_bbox, + description="{tight, standard} 'tight' is incompatible with generating frames " + "for animation" + ), + _Param( + "savefig.pad_inches", + default=0.1, + validator=validate_float, + description="padding to be used, when bbox is set to 'tight'" + ), + _Param( + "savefig.directory", + default="~", + validator=_validate_pathlike, + description="default directory in savefig dialog, gets updated after " + "interactive saves, unless set to the empty string (i.e. the " + "current directory); use '.' to start at the current directory but " + "update after interactive saves" + ), + _Param( + "savefig.transparent", + default=False, + validator=validate_bool, + description="whether figures are saved with a transparent background by default" + + ), + _Param( + "savefig.orientation", + default="portrait", + validator=["landscape", "portrait"], + description="orientation of saved figure, for PostScript output only" + ), + _Param( + "macosx.window_mode", + default="system", + validator=["system", "tab", "window"], + description="How to open new figures (system, tab, window) system uses " + "the MacOS system preferences" + ), + _Param( + "tk.window_focus", + default=False, + validator=validate_bool, + description="Maintain shell focus for TkAgg" + ), + _Param( + "ps.papersize", + default="letter", + validator=_ignorecase( + ["figure", "letter", "legal", "ledger", + *[f"{ab}{i}" for ab in "ab" for i in range(11)], + ], + ), + description="{figure, letter, legal, ledger, A0-A10, B0-B10}" + ), + _Param( + "ps.useafm", + default=False, + validator=validate_bool, + description="use AFM fonts, results in small files" + ), + _Param( + "ps.usedistiller", + default=None, + validator=validate_ps_distiller, + description="{ghostscript, xpdf, None} Experimental: may produce smaller " + "files. xpdf intended for production of publication quality files, " + "but requires ghostscript, xpdf and ps2eps" + ), + _Param( + "ps.distiller.res", + default=6000, + validator=validate_int, description="dpi" + ), + _Param( + "ps.fonttype", + default=3, + validator=validate_fonttype, + description="Output Type 3 (Type3) or Type 42 (TrueType)" + ), + _Param( + "pdf.compression", + default=6, + validator=validate_int, + description="integer from 0 to 9 0 disables compression (good for debugging)" + ), + _Param( + "pdf.fonttype", + default=3, + validator=validate_fonttype, + description="Output Type 3 (Type3) or Type 42 (TrueType)" + ), + _Param( + "pdf.use14corefonts", + default=False, + validator=validate_bool + ), + _Param( + "pdf.inheritcolor", + default=False, + validator=validate_bool + ), + _Param( + "svg.image_inline", + default=True, + validator=validate_bool, + description="Write raster image data directly into the SVG file" + ), + _Param( + "svg.fonttype", + default="path", + validator=["none", "path"], + description="How to handle SVG fonts: " + "path: Embed characters as paths -- supported by most SVG " + " renderers" + "none: Assume fonts are installed on the machine where the SVG " + "will be viewed." + ), + _Param( + "svg.hashsalt", + default=None, + validator=validate_string_or_None, + description="If not None, use this string as hash salt instead of uuid4" + ), + _Param( + "svg.id", + default=None, + validator=validate_string_or_None, + description="If not None, use this string as the value for the `id` attribute " + "in the top tag" + ), + _Param( + "pgf.rcfonts", + default=True, + validator=validate_bool + ), + _Param( + "pgf.preamble", + default="", + validator=validate_string, + description="See text.latex.preamble for documentation" + ), + _Param( + "pgf.texsystem", + default="xelatex", + validator=["xelatex", "lualatex", "pdflatex"] + ), + _Param( + "docstring.hardcopy", + default=False, + validator=validate_bool, + description="set this when you want to generate hardcopy docstring" + ), + _Param( + "keymap.fullscreen", + default=["f", "ctrl+f"], + validator=validate_stringlist, + description="toggling" + ), + _Param( + "keymap.home", + default=["h", "r", "home"], + validator=validate_stringlist, + description="home or reset mnemonic" + ), + _Param( + "keymap.back", + default=["left", "c", "backspace", "MouseButton.BACK"], + validator=validate_stringlist, description="forward / backward keys" + ), + _Param( + "keymap.forward", + default=["right", "v", "MouseButton.FORWARD"], + validator=validate_stringlist, + description="for quick navigation" + ), + _Param( + "keymap.pan", + default=["p"], + validator=validate_stringlist, description="pan mnemonic" + ), + _Param( + "keymap.zoom", + default=["o"], + validator=validate_stringlist, description="zoom mnemonic" + ), + _Param( + "keymap.save", + default=["s", "ctrl+s"], + validator=validate_stringlist, + description="saving current figure" + ), + _Param( + "keymap.help", + default=["f1"], + validator=validate_stringlist, + description="display help about active tools" + ), + _Param( + "keymap.quit", + default=["ctrl+w", "cmd+w", "q"], + validator=validate_stringlist, + description="close the current figure" + ), + _Param( + "keymap.quit_all", + default=[], + validator=validate_stringlist, description="close all figures" + ), + _Param( + "keymap.grid", + default=["g"], + validator=validate_stringlist, + description="switching on/off major grids in current axes" + ), + _Param( + "keymap.grid_minor", + default=["G"], + validator=validate_stringlist, + description="switching on/off minor grids in current axes" + ), + _Param( + "keymap.yscale", + default=["l"], + validator=validate_stringlist, + description="toggle scaling of y-axes ('log'/'linear')" + ), + _Param( + "keymap.xscale", + default=["k", "L"], + validator=validate_stringlist, + description="toggle scaling of x-axes ('log'/'linear')" + ), + _Param( + "keymap.copy", + default=["ctrl+c", "cmd+c"], + validator=validate_stringlist, + description="copy figure to clipboard" + ), + _Param( + "animation.html", + default="none", + validator=["html5", "jshtml", "none"], + description="How to display the animation as HTML in the IPython notebook: " + "- 'html5' uses HTML5 video tag " + "- 'jshtml' creates a JavaScript animation" + ), + _Param( + "animation.writer", + default="ffmpeg", + validator=validate_string, + description="MovieWriter 'backend' to use" + ), + _Param( + "animation.codec", + default="h264", + validator=validate_string, + description="Codec to use for writing movie" + ), + _Param( + "animation.bitrate", + default=-1, + validator=validate_int, + description="Controls size/quality trade-off for movie. -1 implies let " + "utility auto-determine" + ), + _Param("animation.frame_format", "png", + ["png", "jpeg", "tiff", "raw", "rgba", "ppm", "sgi", "bmp", "pbm", "svg"], + description="Controls frame format used by temp files" + ), + _Param( + "animation.ffmpeg_path", + default="ffmpeg", + validator=_validate_pathlike, + description="Path to ffmpeg binary. Unqualified paths are resolved by " + "subprocess.Popen." + ), + _Param( + "animation.ffmpeg_args", + default=[], + validator=validate_stringlist, + description="Additional arguments to pass to ffmpeg" + ), + _Param( + "animation.convert_path", + default="convert", + validator=_validate_pathlike, + description="Path to ImageMagick's convert binary. Unqualified paths are " + "resolved by subprocess.Popen, except that on Windows, we look up " + "an install of ImageMagick in the registry (as convert is also the " + "name of a system tool)." + ), + _Param( + "animation.convert_args", + default=["-layers", "OptimizePlus"], + validator=validate_stringlist, + description="Additional arguments to pass to convert" + ), + _Param( + "animation.embed_limit", + default=20.0, + validator=validate_float, + description="Limit, in MB, of size of base64 encoded animation in HTML (i.e. " + "IPython notebook)" + ), + _Param( + "_internal.classic_mode", + default=False, + validator=validate_bool + ), + _Param("backend", None, validate_backend), +] + + +def _generate_rst(): # pragma: no cover + """ + Generate rst documentation for rcParams. + + Note: The style is very simple, but will be refined later. + """ + + text = """\ +.. + autogenerated rcParams documentation. Update via + > python -c "from matplotlib import rcsetup; rcsetup._write_rcParam_rst()" + +""" + for param in _params: + text += f""" +.. _rcparam_{param.name.replace('.', '_')}: + +{param.name}: ``{param.default!r}`` + {param.description if param.description else "*no description*"} +""" + return text + + +def _write_rcParam_rst(): # pragma: no cover + """Write the generated rst documentation to /users/_rcparams_generated.rst.""" + docpath = Path(os.path.dirname(__file__)) / "../../doc" + if not docpath.exists(): + raise RuntimeError("Cannot find the doc/api/ directory") + + (docpath / "users/_rcparams_generated.rst").write_text(_generate_rst()) diff --git a/lib/matplotlib/rcsetup.pyi b/lib/matplotlib/rcsetup.pyi index c6611845723d..120c0c93bec9 100644 --- a/lib/matplotlib/rcsetup.pyi +++ b/lib/matplotlib/rcsetup.pyi @@ -33,6 +33,7 @@ def validate_string_or_None(s: Any) -> str | None: ... def validate_stringlist(s: Any) -> list[str]: ... def validate_int(s: Any) -> int: ... def validate_int_or_None(s: Any) -> int | None: ... +def validate_intlist(s: Any) -> list[int]: ... def validate_float(s: Any) -> float: ... def validate_float_or_None(s: Any) -> float | None: ... def validate_floatlist(s: Any) -> list[float]: ... diff --git a/lib/matplotlib/sankey.pyi b/lib/matplotlib/sankey.pyi index 33565b998a9c..083d590559ca 100644 --- a/lib/matplotlib/sankey.pyi +++ b/lib/matplotlib/sankey.pyi @@ -2,7 +2,7 @@ from matplotlib.axes import Axes from collections.abc import Callable, Iterable from typing import Any -from typing_extensions import Self # < Py 3.11 +from typing import Self import numpy as np diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 4517b8946b03..f6ccc42442d6 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -75,9 +75,20 @@ def __init__(self, axis): The following note is for scale implementers. For back-compatibility reasons, scales take an `~matplotlib.axis.Axis` - object as first argument. However, this argument should not - be used: a single scale object should be usable by multiple - `~matplotlib.axis.Axis`\es at the same time. + object as the first argument. + + .. deprecated:: 3.11 + + The *axis* parameter is now optional, i.e. matplotlib is compatible + with `.ScaleBase` subclasses that do not take an *axis* parameter. + + The *axis* parameter is pending-deprecated. It will be deprecated + in matplotlib 3.13, and removed in matplotlib 3.15. + + 3rd-party scales are recommended to remove the *axis* parameter now + if they can afford to restrict compatibility to matplotlib >= 3.11 + already. Otherwise, they may keep the *axis* parameter and remove it + in time for matplotlib 3.13. """ def get_transform(self): @@ -236,6 +247,12 @@ def __init__(self, axis, functions): ---------- axis : `~matplotlib.axis.Axis` The axis for the scale. + + .. note:: + This parameter is unused and will be removed in an imminent release. + It can already be left out because of special preprocessing, + so that ``FuncScale(functions)`` is valid. + functions : (callable, callable) two-tuple of the forward and inverse functions for the scale. The forward function must be monotonic. @@ -336,6 +353,12 @@ def __init__(self, axis=None, *, base=10, subs=None, nonpositive="clip"): ---------- axis : `~matplotlib.axis.Axis` The axis for the scale. + + .. note:: + This parameter is unused and about to be removed in the future. + It can already now be left out because of special preprocessing, + so that ``LogScale(base=2)`` is valid. + base : float, default: 10 The base of the logarithm. nonpositive : {'clip', 'mask'}, default: 'clip' @@ -485,6 +508,14 @@ class SymmetricalLogScale(ScaleBase): Parameters ---------- + axis : `~matplotlib.axis.Axis` + The axis for the scale. + + .. note:: + This parameter is unused and about to be removed in the future. + It can already now be left out because of special preprocessing, + so that ``SymmetricalLocSacle(base=2)`` is valid. + base : float, default: 10 The base of the logarithm. @@ -606,6 +637,14 @@ def __init__(self, axis=None, *, linear_width=1.0, """ Parameters ---------- + axis : `~matplotlib.axis.Axis` + The axis for the scale. + + .. note:: + This parameter is unused and about to be removed in the future. + It can already now be left out because of special preprocessing, + so that ``AsinhScale()`` is valid. + linear_width : float, default: 1 The scale parameter (elsewhere referred to as :math:`a_0`) defining the extent of the quasi-linear region, @@ -706,7 +745,13 @@ def __init__(self, axis=None, nonpositive='mask', *, Parameters ---------- axis : `~matplotlib.axis.Axis` - Currently unused. + The axis for the scale. + + .. note:: + This parameter is unused and about to be removed in the future. + It can already now be left out because of special preprocessing, + so that ``LogitScale()`` is valid. + nonpositive : {'mask', 'clip'} Determines the behavior for values beyond the open interval ]0, 1[. They can either be masked as invalid, or clipped to a number very @@ -764,6 +809,20 @@ def limit_range_for_scale(self, vmin, vmax, minpos): 'functionlog': FuncScaleLog, } +# caching of signature info +# For backward compatibility, the built-in scales will keep the *axis* parameter +# in their constructors until matplotlib 3.15, i.e. as long as the *axis* parameter +# is still supported. +_scale_has_axis_parameter = { + 'linear': True, + 'log': True, + 'symlog': True, + 'asinh': True, + 'logit': True, + 'function': True, + 'functionlog': True, +} + def get_scale_names(): """Return the names of the available scales.""" @@ -780,7 +839,11 @@ def scale_factory(scale, axis, **kwargs): axis : `~matplotlib.axis.Axis` """ scale_cls = _api.check_getitem(_scale_mapping, scale=scale) - return scale_cls(axis, **kwargs) + + if _scale_has_axis_parameter[scale]: + return scale_cls(axis, **kwargs) + else: + return scale_cls(**kwargs) if scale_factory.__doc__: @@ -799,6 +862,20 @@ def register_scale(scale_class): """ _scale_mapping[scale_class.name] = scale_class + # migration code to handle the *axis* parameter + has_axis_parameter = "axis" in inspect.signature(scale_class).parameters + _scale_has_axis_parameter[scale_class.name] = has_axis_parameter + if has_axis_parameter: + _api.warn_deprecated( + "3.11", + message=f"The scale {scale_class.__qualname__!r} uses an 'axis' parameter " + "in the constructors. This parameter is pending-deprecated since " + "matplotlib 3.11. It will be fully deprecated in 3.13 and removed " + "in 3.15. Starting with 3.11, 'register_scale()' accepts scales " + "without the *axis* parameter.", + pending=True, + ) + def _get_scale_docs(): """ diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index b5f10d851182..7b46b3145e2b 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -84,6 +84,11 @@ figure. This overwrites the caption given in the content, when the plot is generated from a file. +``:code-caption:`` : str + If specified, the option's argument will be used as a caption for the + code block (when ``:include-source:`` is used). This is added as the + ``:caption:`` option to the ``.. code-block::`` directive. + Additionally, this directive supports all the options of the `image directive `_, except for ``:target:`` (since plot will add its own target). These include @@ -281,6 +286,7 @@ class PlotDirective(Directive): 'context': _option_context, 'nofigs': directives.flag, 'caption': directives.unchanged, + 'code-caption': directives.unchanged, } def run(self): @@ -952,8 +958,11 @@ def run(arguments, content, options, state_machine, state, lineno): if is_doctest: lines = ['', *code_piece.splitlines()] else: - lines = ['.. code-block:: python', '', - *textwrap.indent(code_piece, ' ').splitlines()] + lines = ['.. code-block:: python'] + if 'code-caption' in options: + code_caption = options['code-caption'].replace('\n', ' ') + lines.append(f' :caption: {code_caption}') + lines.extend(['', *textwrap.indent(code_piece, ' ').splitlines()]) source_code = "\n".join(lines) else: source_code = "" diff --git a/lib/matplotlib/sphinxext/roles.py b/lib/matplotlib/sphinxext/roles.py index c3e57ebc3aec..0b696f830543 100644 --- a/lib/matplotlib/sphinxext/roles.py +++ b/lib/matplotlib/sphinxext/roles.py @@ -78,6 +78,22 @@ def _depart_query_reference_node(self, node): self.depart_literal(node) +# We sometimes want to use special notation in rcParam references, e.g. +# to use wildcards. This mapping maps such special notations to real rcParams, +# typically to the first relevant parameter in the group. +_RC_WILDCARD_LINK_MAPPING = { + "animation.[name-of-encoder]_args": "animation.ffmpeg_args", + "figure.subplot.*": "figure.subplot.left", + "figure.subplot.[name]": "figure.subplot.left", + "font.*": "font.family", + "lines.*": "lines.linewidth", + "patch.*": "patch.edgecolor", + "grid.major.*": "grid.major.color", + "grid.minor.*": "grid.minor.color", + "grid.*": "grid.color", +} + + def _rcparam_role(name, rawtext, text, lineno, inliner, options=None, content=None): """ Sphinx role ``:rc:`` to highlight and link ``rcParams`` entries. @@ -89,7 +105,8 @@ def _rcparam_role(name, rawtext, text, lineno, inliner, options=None, content=No # Generate a pending cross-reference so that Sphinx will ensure this link # isn't broken at some point in the future. title = f'rcParams["{text}"]' - target = 'matplotlibrc-sample' + rc_param_name = _RC_WILDCARD_LINK_MAPPING.get(text, text) + target = f'rcparam_{rc_param_name.replace(".", "_")}' ref_nodes, messages = inliner.interpreted(title, f'{title} <{target}>', 'ref', lineno) diff --git a/lib/matplotlib/spines.py b/lib/matplotlib/spines.py index 9732a2f3347a..741491b3dc58 100644 --- a/lib/matplotlib/spines.py +++ b/lib/matplotlib/spines.py @@ -232,12 +232,13 @@ def _clear(self): """ self._position = None # clear position - def _adjust_location(self): - """Automatically set spine bounds to the view interval.""" - - if self.spine_type == 'circle': - return + def _get_bounds_or_viewLim(self): + """ + Get the bounds of the spine. + If self._bounds is None, return self.axes.viewLim.intervalx + or self.axes.viewLim.intervaly based on self.spine_type + """ if self._bounds is not None: low, high = self._bounds elif self.spine_type in ('left', 'right'): @@ -245,7 +246,16 @@ def _adjust_location(self): elif self.spine_type in ('top', 'bottom'): low, high = self.axes.viewLim.intervalx else: - raise ValueError(f'unknown spine spine_type: {self.spine_type}') + raise ValueError(f'spine_type: {self.spine_type} not supported') + return low, high + + def _adjust_location(self): + """Automatically set spine bounds to the view interval.""" + + if self.spine_type == 'circle': + return + + low, high = self._get_bounds_or_viewLim() if self._patch_type == 'arc': if self.spine_type in ('bottom', 'top'): @@ -424,7 +434,7 @@ def set_bounds(self, low=None, high=None): 'set_bounds() method incompatible with circular spines') if high is None and np.iterable(low): low, high = low - old_low, old_high = self.get_bounds() or (None, None) + old_low, old_high = self._get_bounds_or_viewLim() if low is None: low = old_low if high is None: diff --git a/lib/matplotlib/stackplot.py b/lib/matplotlib/stackplot.py index bd11558b0da9..25bb2f45a0c4 100644 --- a/lib/matplotlib/stackplot.py +++ b/lib/matplotlib/stackplot.py @@ -6,17 +6,16 @@ (https://stackoverflow.com/users/66549/doug) """ -import itertools import numpy as np -from matplotlib import _api +from matplotlib import cbook, collections, _api, _style_helpers __all__ = ['stackplot'] def stackplot(axes, x, *args, - labels=(), colors=None, hatch=None, baseline='zero', + labels=(), colors=None, baseline='zero', **kwargs): """ Draw a stacked area plot or a streamgraph. @@ -55,23 +54,26 @@ def stackplot(axes, x, *args, If not specified, the colors from the Axes property cycle will be used. - hatch : list of str, default: None - A sequence of hatching styles. See - :doc:`/gallery/shapes_and_collections/hatch_style_reference`. - The sequence will be cycled through for filling the - stacked areas from bottom to top. - It need not be exactly the same length as the number - of provided *y*, in which case the styles will repeat from the - beginning. - - .. versionadded:: 3.9 - Support for list input - data : indexable object, optional DATA_PARAMETER_PLACEHOLDER **kwargs - All other keyword arguments are passed to `.Axes.fill_between`. + All other keyword arguments are passed to `.Axes.fill_between`. The + following parameters additionally accept a sequence of values + corresponding to the *y* datasets: + + - *hatch* + - *edgecolor* + - *facecolor* + - *linewidth* + - *linestyle* + + .. versionadded:: 3.9 + Allowing a sequence of strings for *hatch*. + + .. versionadded:: 3.11 + Allowing sequences of values in above listed `.Axes.fill_between` + parameters. Returns ------- @@ -83,15 +85,13 @@ def stackplot(axes, x, *args, y = np.vstack(args) labels = iter(labels) - if colors is not None: - colors = itertools.cycle(colors) - else: - colors = (axes._get_lines.get_next_color() for _ in y) + if colors is None: + colors = [axes._get_lines.get_next_color() for _ in y] + + kwargs = cbook.normalize_kwargs(kwargs, collections.PolyCollection) + kwargs.setdefault('facecolor', colors) - if hatch is None or isinstance(hatch, str): - hatch = itertools.cycle([hatch]) - else: - hatch = itertools.cycle(hatch) + kwargs, style_gen = _style_helpers.style_generator(kwargs) # Assume data passed has not been 'stacked', so stack it here. # We'll need a float buffer for the upcoming calculations. @@ -130,18 +130,14 @@ def stackplot(axes, x, *args, # Color between x = 0 and the first array. coll = axes.fill_between(x, first_line, stack[0, :], - facecolor=next(colors), - hatch=next(hatch), label=next(labels, None), - **kwargs) + **next(style_gen), **kwargs) coll.sticky_edges.y[:] = [0] r = [coll] # Color between array i-1 and array i for i in range(len(y) - 1): r.append(axes.fill_between(x, stack[i, :], stack[i + 1, :], - facecolor=next(colors), - hatch=next(hatch), label=next(labels, None), - **kwargs)) + **next(style_gen), **kwargs)) return r diff --git a/lib/matplotlib/streamplot.py b/lib/matplotlib/streamplot.py index ece8bebf8192..725fff7b23fd 100644 --- a/lib/matplotlib/streamplot.py +++ b/lib/matplotlib/streamplot.py @@ -6,7 +6,7 @@ import numpy as np import matplotlib as mpl -from matplotlib import _api, cm, patches +from matplotlib import _api, colorizer, patches import matplotlib.colors as mcolors import matplotlib.collections as mcollections import matplotlib.lines as mlines @@ -228,7 +228,7 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, if use_multicolor_lines: if norm is None: norm = mcolors.Normalize(color.min(), color.max()) - cmap = cm._ensure_cmap(cmap) + cmap = colorizer._ensure_cmap(cmap) streamlines = [] arrows = [] diff --git a/lib/matplotlib/table.py b/lib/matplotlib/table.py index 370ce9fe922f..91dddba6c31f 100644 --- a/lib/matplotlib/table.py +++ b/lib/matplotlib/table.py @@ -240,6 +240,13 @@ class Table(Artist): """ A table of cells. + .. note:: + + ``table()`` has some fundamental design limitations and will not be + developed further. If you need more functionality, consider + `blume `__. + + The table consists of a grid of cells, which are indexed by (row, column). For a simple table, you'll have a full grid of cells with indices from @@ -658,6 +665,12 @@ def table(ax, """ Add a table to an `~.axes.Axes`. + .. note:: + + ``table()`` has some fundamental design limitations and will not be + developed further. If you need more functionality, consider + `blume `__. + At least one of *cellText* or *cellColours* must be specified. These parameters must be 2D lists, in which the outer lists define the rows and the inner list define the column values per row. Each row must have the diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index d6affb1b039f..a838a6b09b32 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -54,7 +54,7 @@ def setup(): def subprocess_run_for_testing(command, env=None, timeout=60, stdout=None, stderr=None, check=False, text=True, - capture_output=False): + capture_output=False, **kwargs): """ Create and run a subprocess. @@ -92,12 +92,19 @@ def subprocess_run_for_testing(command, env=None, timeout=60, stdout=None, """ if capture_output: stdout = stderr = subprocess.PIPE + # Add CREATE_NO_WINDOW flag on Windows to prevent console window overhead + # This is added in an attempt to fix flaky timeouts of subprocesses on Windows + if sys.platform == 'win32': + if 'creationflags' not in kwargs: + kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW + else: + kwargs['creationflags'] |= subprocess.CREATE_NO_WINDOW try: proc = subprocess.run( command, env=env, timeout=timeout, check=check, stdout=stdout, stderr=stderr, - text=text + text=text, **kwargs ) except BlockingIOError: if sys.platform == "cygwin": @@ -105,6 +112,16 @@ def subprocess_run_for_testing(command, env=None, timeout=60, stdout=None, import pytest pytest.xfail("Fork failure") raise + except subprocess.CalledProcessError as e: + if e.stdout: + _log.error(f"Subprocess output:\n{e.stdout}") + if e.stderr: + _log.error(f"Subprocess error:\n{e.stderr}") + raise e + if proc.stdout: + _log.debug(f"Subprocess output:\n{proc.stdout}") + if proc.stderr: + _log.debug(f"Subprocess error:\n{proc.stderr}") return proc @@ -134,13 +151,26 @@ def subprocess_run_helper(func, *args, timeout, extra_env=None): f"_module = importlib.util.module_from_spec(_spec);" f"_spec.loader.exec_module(_module);" f"_module.{target}()", - *args + *args, ], - env={**os.environ, "SOURCE_DATE_EPOCH": "0", **(extra_env or {})}, - timeout=timeout, check=True, + env={ + **os.environ, + "SOURCE_DATE_EPOCH": "0", + # subprocess_run_helper sets SOURCE_DATE_EPOCH=0 above, so for a dirty tree, + # the version will have the date 19700101 which breaks pickle tests with a + # warning if the working tree is dirty. + # + # This will also avoid at least one additional subprocess call for + # setuptools-scm query git, so we tell the subprocess what version + # to report as the test process. + "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MATPLOTLIB": mpl.__version__, + **(extra_env or {}), + }, + timeout=timeout, + check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - text=True + text=True, ) return proc diff --git a/lib/matplotlib/testing/__init__.pyi b/lib/matplotlib/testing/__init__.pyi index 7763cb6a9769..accf973615fa 100644 --- a/lib/matplotlib/testing/__init__.pyi +++ b/lib/matplotlib/testing/__init__.pyi @@ -16,6 +16,7 @@ def subprocess_run_for_testing( *, text: Literal[True], capture_output: bool = ..., + **kwargs, ) -> subprocess.CompletedProcess[str]: ... @overload def subprocess_run_for_testing( @@ -27,6 +28,7 @@ def subprocess_run_for_testing( check: bool = ..., text: Literal[False] = ..., capture_output: bool = ..., + **kwargs, ) -> subprocess.CompletedProcess[bytes]: ... @overload def subprocess_run_for_testing( @@ -38,6 +40,7 @@ def subprocess_run_for_testing( check: bool = ..., text: bool = ..., capture_output: bool = ..., + **kwargs, ) -> subprocess.CompletedProcess[bytes] | subprocess.CompletedProcess[str]: ... def subprocess_run_helper( func: Callable[[], None], diff --git a/lib/matplotlib/testing/widgets.py b/lib/matplotlib/testing/widgets.py index 3962567aa7c0..c528ffb2537c 100644 --- a/lib/matplotlib/testing/widgets.py +++ b/lib/matplotlib/testing/widgets.py @@ -8,6 +8,8 @@ from unittest import mock +from matplotlib import _api +from matplotlib.backend_bases import MouseEvent, KeyEvent import matplotlib.pyplot as plt @@ -24,6 +26,7 @@ def noop(*args, **kwargs): pass +@_api.deprecated("3.11", alternative="MouseEvent or KeyEvent") def mock_event(ax, button=1, xdata=0, ydata=0, key=None, step=1): r""" Create a mock event that can stand in for `.Event` and its subclasses. @@ -65,6 +68,7 @@ def mock_event(ax, button=1, xdata=0, ydata=0, key=None, step=1): return event +@_api.deprecated("3.11", alternative="callbacks.process(event)") def do_event(tool, etype, button=1, xdata=0, ydata=0, key=None, step=1): """ Trigger an event on the given tool. @@ -105,15 +109,12 @@ def click_and_drag(tool, start, end, key=None): An optional key that is pressed during the whole operation (see also `.KeyEvent`). """ - if key is not None: - # Press key - do_event(tool, 'on_key_press', xdata=start[0], ydata=start[1], - button=1, key=key) + ax = tool.ax + if key is not None: # Press key + KeyEvent._from_ax_coords("key_press_event", ax, start, key)._process() # Click, move, and release mouse - do_event(tool, 'press', xdata=start[0], ydata=start[1], button=1) - do_event(tool, 'onmove', xdata=end[0], ydata=end[1], button=1) - do_event(tool, 'release', xdata=end[0], ydata=end[1], button=1) - if key is not None: - # Release key - do_event(tool, 'on_key_release', xdata=end[0], ydata=end[1], - button=1, key=key) + MouseEvent._from_ax_coords("button_press_event", ax, start, 1)._process() + MouseEvent._from_ax_coords("motion_notify_event", ax, end, 1)._process() + MouseEvent._from_ax_coords("button_release_event", ax, end, 1)._process() + if key is not None: # Release key + KeyEvent._from_ax_coords("key_release_event", ax, end, key)._process() diff --git a/lib/matplotlib/tests/baseline_images/dviread/lualatex.json b/lib/matplotlib/tests/baseline_images/dviread/lualatex.json new file mode 100644 index 000000000000..8f2d95017ec7 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/dviread/lualatex.json @@ -0,0 +1 @@ +[{"text": [[5046272, 4128768, "A", "lmroman10-regular.otf", 9.96, {}], [5756027, 4128768, "L", "lmroman10-regular.otf", 9.96, {}], [5929697, 4012179, "A", "lmroman7-regular.otf", 6.97, {}], [6218125, 4128768, "T", "lmroman10-regular.otf", 9.96, {}], [6582045, 4269998, "E", "lmroman10-regular.otf", 9.96, {}], [6946425, 4128768, "X", "lmroman10-regular.otf", 9.96, {}], [7656180, 4128768, "d", "DejaVuSans.ttf", 9.96, {"extend": 1.25, "slant": 0.25, "embolden": 0.25}], [8072180, 4128768, "o", "DejaVuSans.ttf", 9.96, {"extend": 1.25, "slant": 0.25, "embolden": 0.25}], [8473140, 4128768, "c", "DejaVuSans.ttf", 9.96, {"extend": 1.25, "slant": 0.25, "embolden": 0.25}], [8833460, 4128768, ".", "DejaVuSans.ttf", 9.96, {"extend": 1.25, "slant": 0.25, "embolden": 0.25}]], "boxes": []}, {"text": [[13686374, 5056284, "\u03c0", "cmmi5.pfb", 4.98, {}], [13716923, 5390308, "2", "cmr5.pfb", 4.98, {}], [13355110, 5463127, "integraldisplay", "cmex10.pfb", 9.96, {}], [13406537, 7324364, "0", "cmr7.pfb", 6.97, {}], [14010471, 5627696, "parenleftBig", "cmex10.pfb", 9.96, {}], [14937513, 5911796, "x", "cmmi10.pfb", 9.96, {}], [14480510, 6804696, "s", "lmroman10-regular.otf", 9.96, {}], [14738721, 6804696, "i", "lmroman10-regular.otf", 9.96, {}], [14920911, 6804696, "n", "lmroman10-regular.otf", 9.96, {}], [15394516, 6804696, "x", "cmmi10.pfb", 9.96, {}], [15847715, 5627696, "parenrightBig", "cmex10.pfb", 9.96, {}], [16239111, 5763501, "2", "cmr7.pfb", 6.97, {}], [16642338, 6355152, "d", "lmroman10-regular.otf", 9.96, {}], [17006718, 6355152, "x", "cmmi10.pfb", 9.96, {}]], "boxes": [[13686374, 5130818, 26213, 284106], [14480510, 6204418, 26213, 1288562]]}] diff --git a/lib/matplotlib/tests/baseline_images/dviread/pdflatex.json b/lib/matplotlib/tests/baseline_images/dviread/pdflatex.json new file mode 100644 index 000000000000..4754b722aa58 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/dviread/pdflatex.json @@ -0,0 +1 @@ +[{"text": [[5046272, 4128768, "A", "cmr10.pfb", 9.96, {}], [5756246, 4128768, "L", "cmr10.pfb", 9.96, {}], [5929917, 3994421, "A", "cmr7.pfb", 6.97, {}], [6218464, 4128768, "T", "cmr10.pfb", 9.96, {}], [6582530, 4269852, "E", "cmr10.pfb", 9.96, {}], [6946620, 4128768, "X", "cmr10.pfb", 9.96, {}], [7656594, 4128768, "d", "cmr10.pfb", 9.96, {}], [8020684, 4128768, "o", "cmr10.pfb", 9.96, {}], [8366570, 4128768, "c", "cmr10.pfb", 9.96, {}], [8657841, 4128768, ".", "cmr10.pfb", 9.96, {}]], "boxes": []}, {"text": [[13686591, 5056284, "\u03c0", "cmmi5.pfb", 4.98, {}], [13717140, 5390308, "2", "cmr5.pfb", 4.98, {}], [13355327, 5463127, "integraldisplay", "cmex10.pfb", 9.96, {}], [13406754, 7324364, "0", "cmr7.pfb", 6.97, {}], [14010688, 5627696, "parenleftBig", "cmex10.pfb", 9.96, {}], [14937658, 5911796, "x", "cmmi10.pfb", 9.96, {}], [14480727, 6804696, "s", "cmr10.pfb", 9.96, {}], [14739230, 6804696, "i", "cmr10.pfb", 9.96, {}], [14921275, 6804696, "n", "cmr10.pfb", 9.96, {}], [15394589, 6804696, "x", "cmmi10.pfb", 9.96, {}], [15847788, 5627696, "parenrightBig", "cmex10.pfb", 9.96, {}], [16239184, 5763501, "2", "cmr7.pfb", 6.97, {}], [16642411, 6355152, "d", "cmr10.pfb", 9.96, {}], [17006501, 6355152, "x", "cmmi10.pfb", 9.96, {}]], "boxes": [[13686591, 5130818, 26213, 284106], [14480727, 6204418, 26213, 1288418]]}] diff --git a/lib/matplotlib/tests/baseline_images/dviread/test.dvi b/lib/matplotlib/tests/baseline_images/dviread/test.dvi deleted file mode 100644 index 93751ffdcba0..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/dviread/test.dvi and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/dviread/test.json b/lib/matplotlib/tests/baseline_images/dviread/test.json deleted file mode 100644 index 0809cb9531f1..000000000000 --- a/lib/matplotlib/tests/baseline_images/dviread/test.json +++ /dev/null @@ -1,94 +0,0 @@ -[ - { - "text": [ - [5046272, 4128768, "T", "cmr10", 9.96], - [5519588, 4128768, "h", "cmr10", 9.96], - [5883678, 4128768, "i", "cmr10", 9.96], - [6065723, 4128768, "s", "cmr10", 9.96], - [6542679, 4128768, "i", "cmr10", 9.96], - [6724724, 4128768, "s", "cmr10", 9.96], - [7201680, 4128768, "a", "cmr10", 9.96], - [7747814, 4128768, "L", "cmr10", 9.96], - [7921485, 3994421, "A", "cmr7", 6.97], - [8210032, 4128768, "T", "cmr10", 9.96], - [8574098, 4269852, "E", "cmr10", 9.96], - [8938188, 4128768, "X", "cmr10", 9.96], - [9648162, 4128768, "t", "cmr10", 9.96], - [9903025, 4128768, "e", "cmr10", 9.96], - [10194296, 4128768, "s", "cmr10", 9.96], - [10452799, 4128768, "t", "cmr10", 9.96], - [10926115, 4128768, "d", "cmr10", 9.96], - [11290205, 4128768, "o", "cmr10", 9.96], - [11636091, 4128768, "c", "cmr10", 9.96], - [11927362, 4128768, "u", "cmr10", 9.96], - [12291452, 4128768, "m", "cmr10", 9.96], - [12837587, 4128768, "e", "cmr10", 9.96], - [13128858, 4128768, "n", "cmr10", 9.96], - [13474743, 4128768, "t", "cmr10", 9.96], - [4063232, 4915200, "f", "cmr10", 9.96], - [4263482, 4915200, "o", "cmr10", 9.96], - [4591163, 4915200, "r", "cmr10", 9.96], - [5066299, 4915200, "t", "cmr10", 9.96], - [5321162, 4915200, "e", "cmr10", 9.96], - [5612433, 4915200, "s", "cmr10", 9.96], - [5870936, 4915200, "t", "cmr10", 9.96], - [6125799, 4915200, "i", "cmr10", 9.96], - [6307844, 4915200, "n", "cmr10", 9.96], - [6671934, 4915200, "g", "cmr10", 9.96], - [7218068, 4915200, "m", "cmr10", 9.96], - [7764203, 4915200, "a", "cmr10", 9.96], - [8091884, 4915200, "t", "cmr10", 9.96], - [8346747, 4915200, "p", "cmr10", 9.96], - [8710837, 4915200, "l", "cmr10", 9.96], - [8892882, 4915200, "o", "cmr10", 9.96], - [9220563, 4915200, "t", "cmr10", 9.96], - [9475426, 4915200, "l", "cmr10", 9.96], - [9657471, 4915200, "i", "cmr10", 9.96], - [9839516, 4915200, "b", "cmr10", 9.96], - [10203606, 4915200, "'", "cmr10", 9.96], - [10385651, 4915200, "s", "cmr10", 9.96], - [10862607, 4915200, "d", "cmr10", 9.96], - [11226697, 4915200, "v", "cmr10", 9.96], - [11572583, 4915200, "i", "cmr10", 9.96], - [11754628, 4915200, "r", "cmr10", 9.96], - [12011311, 4915200, "e", "cmr10", 9.96], - [12302582, 4915200, "a", "cmr10", 9.96], - [12630263, 4915200, "d", "cmr10", 9.96], - [13686591, 6629148, "\u0019", "cmmi5", 4.98], - [13717140, 6963172, "2", "cmr5", 4.98], - [13355327, 7035991, "Z", "cmex10", 9.96], - [13406754, 8897228, "0", "cmr7", 6.97], - [14010688, 7200560, "\u0010", "cmex10", 9.96], - [14937658, 7484660, "x", "cmmi10", 9.96], - [14480727, 8377560, "s", "cmr10", 9.96], - [14739230, 8377560, "i", "cmr10", 9.96], - [14921275, 8377560, "n", "cmr10", 9.96], - [15394589, 8377560, "x", "cmmi10", 9.96], - [15847788, 7200560, "\u0011", "cmex10", 9.96], - [16239184, 7336365, "2", "cmr7", 6.97], - [16642411, 7928016, "d", "cmr10", 9.96], - [17006501, 7928016, "x", "cmmi10", 9.96] - ], - "boxes": [ - [4063232, 5701632, 65536, 22609920], - [13686591, 6703682, 26213, 284106], - [14480727, 7777282, 26213, 1288418] - ] - }, - { - "text": [ - [5046272, 4128768, "a", "cmr10", 9.96], - [5373953, 4128768, "n", "cmr10", 9.96], - [5738043, 4128768, "o", "cmr10", 9.96], - [6065724, 4128768, "t", "cmr10", 9.96], - [6320587, 4128768, "h", "cmr10", 9.96], - [6684677, 4128768, "e", "cmr10", 9.96], - [6975948, 4128768, "r", "cmr10", 9.96], - [7451084, 4128768, "p", "cmr10", 9.96], - [7815174, 4128768, "a", "cmr10", 9.96], - [8142855, 4128768, "g", "cmr10", 9.96], - [8470536, 4128768, "e", "cmr10", 9.96] - ], - "boxes": [] - } -] diff --git a/lib/matplotlib/tests/baseline_images/dviread/test.tex b/lib/matplotlib/tests/baseline_images/dviread/test.tex index 33220fedae3e..4a2d4720c065 100644 --- a/lib/matplotlib/tests/baseline_images/dviread/test.tex +++ b/lib/matplotlib/tests/baseline_images/dviread/test.tex @@ -1,17 +1,19 @@ -% source file for test.dvi \documentclass{article} +\usepackage{iftex} +\iftutex\usepackage{fontspec}\fi % xetex or luatex \pagestyle{empty} + \begin{document} -This is a \LaTeX\ test document\\ -for testing matplotlib's dviread +A \LaTeX { + \iftutex\fontspec{DejaVuSans.ttf}[ + FakeSlant=0.25, FakeStretch=1.25, FakeBold=2.5, Color=0000FF]\fi + doc. +} -\noindent\rule{\textwidth}{1pt} +\newpage \[ \int\limits_0^{\frac{\pi}{2}} \Bigl(\frac{x}{\sin x}\Bigr)^2\,\mathrm{d}x \] \special{Special!} -\newpage -another page - \end{document} diff --git a/lib/matplotlib/tests/baseline_images/dviread/xelatex.json b/lib/matplotlib/tests/baseline_images/dviread/xelatex.json new file mode 100644 index 000000000000..8fb81ddf0c7e --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/dviread/xelatex.json @@ -0,0 +1 @@ +[{"text": [[5046272, 4128768, "A", "lmroman10-regular.otf", 9.96, {}], [5756027, 4128768, "L", "lmroman10-regular.otf", 9.96, {}], [5929697, 4012179, "A", "lmroman7-regular.otf", 6.97, {}], [6218125, 4128768, "T", "lmroman10-regular.otf", 9.96, {}], [6582045, 4269998, "E", "lmroman10-regular.otf", 9.96, {}], [6946425, 4128768, "X", "lmroman10-regular.otf", 9.96, {}], [7656180, 4128768, "d", "DejaVuSans.ttf", 9.96, {"rgba": [0, 0, 255, 255], "extend": 1.25, "slant": 0.25, "embolden": 0.25}], [8176180, 4128768, "o", "DejaVuSans.ttf", 9.96, {"rgba": [0, 0, 255, 255], "extend": 1.25, "slant": 0.25, "embolden": 0.25}], [8677380, 4128768, "c", "DejaVuSans.ttf", 9.96, {"rgba": [0, 0, 255, 255], "extend": 1.25, "slant": 0.25, "embolden": 0.25}], [9127780, 4128768, ".", "DejaVuSans.ttf", 9.96, {"rgba": [0, 0, 255, 255], "extend": 1.25, "slant": 0.25, "embolden": 0.25}]], "boxes": []}, {"text": [[13686374, 5056284, "\u03c0", "cmmi5.pfb", 4.98, {}], [13716923, 5390308, "2", "cmr5.pfb", 4.98, {}], [13355110, 5463127, "integraldisplay", "cmex10.pfb", 9.96, {}], [13406537, 7324364, "0", "cmr7.pfb", 6.97, {}], [14010471, 5627696, "parenleftBig", "cmex10.pfb", 9.96, {}], [14937513, 5911796, "x", "cmmi10.pfb", 9.96, {}], [14480510, 6804696, "s", "lmroman10-regular.otf", 9.96, {}], [14738722, 6804696, "i", "lmroman10-regular.otf", 9.96, {}], [14920912, 6804696, "n", "lmroman10-regular.otf", 9.96, {}], [15394516, 6804696, "x", "cmmi10.pfb", 9.96, {}], [15847715, 5627696, "parenrightBig", "cmex10.pfb", 9.96, {}], [16239111, 5763501, "2", "cmr7.pfb", 6.97, {}], [16642338, 6355152, "d", "lmroman10-regular.otf", 9.96, {}], [17006718, 6355152, "x", "cmmi10.pfb", 9.96, {}]], "boxes": [[13686374, 5130818, 26213, 284106], [14480510, 6204418, 26213, 1288562]]}] diff --git a/lib/matplotlib/tests/baseline_images/test_axes/imshow.pdf b/lib/matplotlib/tests/baseline_images/test_axes/imshow.pdf index 183b072fc312..64f4e3519717 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/imshow.pdf and b/lib/matplotlib/tests/baseline_images/test_axes/imshow.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/imshow.png b/lib/matplotlib/tests/baseline_images/test_axes/imshow.png index c19c4e069b15..d709d9f03f47 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/imshow.png and b/lib/matplotlib/tests/baseline_images/test_axes/imshow.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/imshow.svg b/lib/matplotlib/tests/baseline_images/test_axes/imshow.svg index 3931a1fce23f..b0bcc2358e3a 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/imshow.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/imshow.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-09-29T14:55:05.029228 + image/svg+xml + + + Matplotlib v3.11.0.dev1393+gfd8d60293, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 460.8 345.6 L 460.8 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,50 +35,50 @@ L 369.216 307.584 L 369.216 41.472 L 103.104 41.472 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - +" style="stroke: #000000; stroke-width: 0.8"/> - + - + - + - + - + @@ -76,40 +87,40 @@ L 0 3.5 - +" style="stroke: #000000; stroke-width: 0.8"/> - + - + - + - + - + @@ -117,28 +128,28 @@ L -3.5 0 +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.png b/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.png index cde64b03c7f6..d6cdbd5c07f1 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.png and b/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.svg b/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.svg index d1169e860808..d92dfd8613de 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.svg @@ -1,23 +1,23 @@ - + - + - 2021-03-02T20:09:49.859581 + 2025-09-29T14:55:05.618523 image/svg+xml - Matplotlib v3.3.4.post2495+g8432e3164, https://matplotlib.org/ + Matplotlib v3.11.0.dev1393+gfd8d60293, https://matplotlib.org/ - + @@ -26,7 +26,7 @@ L 460.8 345.6 L 460.8 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -35,29 +35,29 @@ L 369.216 307.584 L 369.216 41.472 L 103.104 41.472 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - +" style="stroke: #000000; stroke-width: 0.8"/> - + - + - +" transform="scale(0.015625)"/> @@ -86,14 +86,14 @@ z - + - + - +" transform="scale(0.015625)"/> - + - + - + - +" transform="scale(0.015625)"/> - + - + - + - +" transform="scale(0.015625)"/> - + - + - + - +" transform="scale(0.015625)"/> - + @@ -264,17 +264,17 @@ z - +" style="stroke: #000000; stroke-width: 0.8"/> - + - + @@ -282,62 +282,62 @@ L -3.5 0 - + - + - + - + - + - + - + - + - + - + - + - + - - + +" clip-path="url(#p94ac23f9ea)" style="fill: none; stroke: #440154; stroke-width: 1.5"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/use_colorizer_keyword.png b/lib/matplotlib/tests/baseline_images/test_axes/use_colorizer_keyword.png index c1c8074ed80c..9d6f8b9c6f63 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/use_colorizer_keyword.png and b/lib/matplotlib/tests/baseline_images/test_axes/use_colorizer_keyword.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colors/light_source_shading_topo.png b/lib/matplotlib/tests/baseline_images/test_colors/light_source_shading_topo.png index 222ebca02d82..dbfb1d425665 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_colors/light_source_shading_topo.png and b/lib/matplotlib/tests/baseline_images/test_colors/light_source_shading_topo.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colors/test_norm_abc.png b/lib/matplotlib/tests/baseline_images/test_colors/test_norm_abc.png index 077365674ac2..7f621591e000 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_colors/test_norm_abc.png and b/lib/matplotlib/tests/baseline_images/test_colors/test_norm_abc.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_compressed_suptitle_colorbar.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_compressed_suptitle_colorbar.png new file mode 100644 index 000000000000..582872077ee3 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_compressed_suptitle_colorbar.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_compressed_supylabel_colorbar.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_compressed_supylabel_colorbar.png new file mode 100644 index 000000000000..9754ac57ad65 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/test_compressed_supylabel_colorbar.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.pdf b/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.pdf index e0baa115a6b3..ad7f762c7a0b 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.pdf and b/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.png b/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.png index c593b2163997..eef9639a3324 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.png and b/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.svg b/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.svg index 406a278f2f3f..1e93d3be191b 100644 --- a/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.svg +++ b/lib/matplotlib/tests/baseline_images/test_image/bbox_image_inverted.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-09-29T15:01:35.415119 + image/svg+xml + + + Matplotlib v3.11.0.dev1393+gfd8d60293, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 460.8 345.6 L 460.8 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,57 +35,57 @@ L 414.72 307.584 L 414.72 41.472 L 57.6 41.472 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - +" style="stroke: #000000; stroke-width: 0.8"/> - + - + - + - + - + - + @@ -83,47 +94,47 @@ L 0 3.5 - +" style="stroke: #000000; stroke-width: 0.8"/> - + - + - + - + - + - + @@ -131,22 +142,22 @@ L -3.5 0 +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> diff --git a/lib/matplotlib/tests/baseline_images/test_image/downsampling.png b/lib/matplotlib/tests/baseline_images/test_image/downsampling.png index 4e68e52d787b..358e5ef2d88f 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/downsampling.png and b/lib/matplotlib/tests/baseline_images/test_image/downsampling.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/image_cliprect.png b/lib/matplotlib/tests/baseline_images/test_image/image_cliprect.png index 4552df3515cd..be33cdd70a99 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/image_cliprect.png and b/lib/matplotlib/tests/baseline_images/test_image/image_cliprect.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/image_cliprect.svg b/lib/matplotlib/tests/baseline_images/test_image/image_cliprect.svg index f8d96c2e312b..50fe1d126a74 100644 --- a/lib/matplotlib/tests/baseline_images/test_image/image_cliprect.svg +++ b/lib/matplotlib/tests/baseline_images/test_image/image_cliprect.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-10-08T04:51:13.682947 + image/svg+xml + + + Matplotlib v3.11.0.dev1425+gb39ccbe8f, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 460.8 345.6 L 460.8 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,236 +35,240 @@ L 369.216 307.584 L 369.216 41.472 L 103.104 41.472 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - +" style="stroke: #000000; stroke-width: 0.8"/> - + - - + + - - - +" transform="scale(0.015625)"/> + + - + - - + + - - - +" transform="scale(0.015625)"/> + + - + - - + + - - - +" transform="scale(0.015625)"/> + + - + - - + + - - - +" transform="scale(0.015625)"/> + + - + - - + + - - - +" transform="scale(0.015625)"/> + + - + - - + + - - - +" transform="scale(0.015625)"/> + + @@ -262,83 +277,83 @@ z - +" style="stroke: #000000; stroke-width: 0.8"/> - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + - + - - + + @@ -346,28 +361,28 @@ L -3.5 0 +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_image/image_composite_alpha.pdf b/lib/matplotlib/tests/baseline_images/test_image/image_composite_alpha.pdf index e7d205bfa8e0..8a09bd6a5a56 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/image_composite_alpha.pdf and b/lib/matplotlib/tests/baseline_images/test_image/image_composite_alpha.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/image_composite_alpha.png b/lib/matplotlib/tests/baseline_images/test_image/image_composite_alpha.png index c9e9f343c5db..3ba3c6e53116 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/image_composite_alpha.png and b/lib/matplotlib/tests/baseline_images/test_image/image_composite_alpha.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/image_composite_alpha.svg b/lib/matplotlib/tests/baseline_images/test_image/image_composite_alpha.svg index 7f1678715ba3..d977b05b0253 100644 --- a/lib/matplotlib/tests/baseline_images/test_image/image_composite_alpha.svg +++ b/lib/matplotlib/tests/baseline_images/test_image/image_composite_alpha.svg @@ -1,12 +1,23 @@ - - + + + + + + 2025-07-10T19:29:50.453941 + image/svg+xml + + + Matplotlib v3.11.0.dev1075+g945334b731, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,112 +35,112 @@ L 468 388.8 L 468 43.2 L 122.4 43.2 z -" style="fill:#008000;"/> +" style="fill: #008000"/> - - + + +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - + - + - + - + - + - + - + - + - + @@ -138,82 +149,82 @@ L 0 4 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - + - + - + - + - + - + - + - + - + @@ -221,8 +232,8 @@ L -4 0 - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_image/imshow_bignumbers.png b/lib/matplotlib/tests/baseline_images/test_image/imshow_bignumbers.png index ec189bb949e5..2b13a801e1c8 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/imshow_bignumbers.png and b/lib/matplotlib/tests/baseline_images/test_image/imshow_bignumbers.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/imshow_bignumbers_real.png b/lib/matplotlib/tests/baseline_images/test_image/imshow_bignumbers_real.png index ce3404488ebb..8de0a4e34204 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/imshow_bignumbers_real.png and b/lib/matplotlib/tests/baseline_images/test_image/imshow_bignumbers_real.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.pdf b/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.pdf index 0342a2baa4b2..d93b352994ab 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.pdf and b/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.png b/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.png index 9e68784cff4f..0fadbb4b1cc4 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.png and b/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.svg b/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.svg index c0385c18467c..e6638ad21189 100644 --- a/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.svg +++ b/lib/matplotlib/tests/baseline_images/test_image/imshow_masked_interpolation.svg @@ -6,11 +6,11 @@ - 2024-04-23T11:45:45.434641 + 2025-09-29T15:01:41.159575 image/svg+xml - Matplotlib v3.9.0.dev1543+gdd88cca65b.d20240423, https://matplotlib.org/ + Matplotlib v3.11.0.dev1393+gfd8d60293, https://matplotlib.org/ @@ -29,167 +29,167 @@ z " style="fill: #ffffff"/> - + +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" id="imageaf5fd25eb2" transform="scale(1 -1) translate(0 -51.12)" x="57.6" y="-54.994689" width="51.12" height="51.12"/> - + +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" id="image4cd56d3e07" transform="scale(1 -1) translate(0 -51.12)" x="118.820571" y="-54.994689" width="51.12" height="51.12"/> - + +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" id="image4a7ce4b4ad" transform="scale(1 -1) translate(0 -51.12)" x="180.041143" y="-54.994689" width="51.12" height="51.12"/> - + +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" id="image91d0d5f5b0" transform="scale(1 -1) translate(0 -51.12)" x="241.261714" y="-54.994689" width="51.12" height="51.12"/> - + +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" id="imagea49606cabe" transform="scale(1 -1) translate(0 -51.12)" x="302.482286" y="-54.994689" width="51.12" height="51.12"/> - + +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" id="imagecc5128dabe" transform="scale(1 -1) translate(0 -51.12)" x="363.702857" y="-54.994689" width="51.12" height="51.12"/> - + +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" id="image30ab40c936" transform="scale(1 -1) translate(0 -51.12)" x="57.6" y="-148.916571" width="51.12" height="51.12"/> - + +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" id="image3f235855ab" transform="scale(1 -1) translate(0 -51.12)" x="118.820571" y="-148.916571" width="51.12" height="51.12"/> - + +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" id="imagea8929fed79" transform="scale(1 -1) translate(0 -51.12)" x="180.041143" y="-148.916571" width="51.12" height="51.12"/> - + +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" id="image7599b6a708" transform="scale(1 -1) translate(0 -51.12)" x="241.261714" y="-148.916571" width="51.12" height="51.12"/> - + +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" id="image1e3120a578" transform="scale(1 -1) translate(0 -51.12)" x="302.482286" y="-148.916571" width="51.12" height="51.12"/> - + +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" id="image6e1531e8f6" transform="scale(1 -1) translate(0 -51.12)" x="363.702857" y="-148.916571" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAAEcAAABHCAYAAABVsFofAAADFklEQVR4nO2bMW4TQRSGd9bjSAiJAoHENSgiUSLBERBVDkBJ4TvQRFyAEqWCM9AiIQpOQBCUFFYUR4BCvBSWnHlvPZ/fGHf5/2p35t8349/vfzPrXafTHw+GrkBfHE+6OiYpmfO+wlvFSdW+vohDvNUY16P48Wu81XmdO0n1mdNnuvGQOACJA8hfL++Yhj4t18eTbun5RZ8pVeY64vrrbAzbN+ZeXXMhzmRrnGJMc129T3CQOID87fJetbN3tvLpavrAgtaqZAcbw49PXLZOMX7Q/qvrhCokDkDiAPLpn/vVTr9ckl9LLvJaasV/cO11y4IXr5vKHIDEAeTvv+6aBtp5linp0/jN4dv18YvPR/UYDTvr8Y45xn398J3pm315Vo1BNlPmACQOQOIA0tMPL43pEnnQ1Jz43TXXsdgy2zJmtG768UdcnM0Nh8QB5J8Xt01DMtapX+jtRykftSr/vB63dcvcyI7KHIDEAUgcQD5f3DINxq/oa38e4zJvcOdValucehhb80ZbEKEKiQPIfy+m9V6fupSfZV+DHXcdb2Qr5MIYEEKZA5A4AIkDyP0C3sJxJhyi9SJam7ZwB6gro8qx7d5jzYM7fz0rj0PiACQOIOeFvw8Adun5ES9tPKQYXbehdtBcsD6VvHhdoTqqzAFIHECeNtgKl3KIwVuA2NhtcerEUQw91NsNEgcgcQB5eu5agjVg7N0G7g7jjeLsaW5lffLXKXMAEgeQD87hzrdlp7vzzroecy9WbeAODf8EuvGQOACJA8gHi/r7KS3e/fh+tj5+9PwYYrZs7ethiPvpZGa6Do+ON/K2xVTmACQOID1+8ir8lAuXxL6+07Qxbef+lnZ6qlfwKB1kqzgkDkDiAPL07Ldt4Zdi1ofR+uMxqg17q09FJ3zlLeMrcwASB5An8wvTMPD7tZuPidd1297ZjcXsnCVwSY7PjbYAyhyAxAFIHEAe5me2pY/WFacr1JUUrQE9f1cpWp/8XKLj65fAOCQOIF/N5/XeBlslXK4Lbov9yGZoKze3qAXdZ1LmACQOQOIA/gFzjLJEhG9/CQAAAABJRU5ErkJggg==" id="image71ded072da" transform="scale(1 -1) translate(0 -51.12)" x="57.6" y="-242.838454" width="51.12" height="51.12"/> - + +iVBORw0KGgoAAAANSUhEUgAAABQAAAAUCAYAAACNiR0NAAABsklEQVR4nI2UMY7UQBBF36/uFUgzntEEJETADVaCG5CTbIiIOQCChCvABQgJCTfaEGlDAsQ1IAEJobFZN4Hb7mrbsyJozfevX69K457RU10kACSGT0NWNFlLAjOqrNngw5CTEcPhkB9OQEaAh1UQVX7UYU8yLcPSqp8kyOwqm3W8OWxmBcr0ESCWdQf2mdjt7+YAFSytQpga07QEJCt1a/eR+fl89YZ2F6bTNUW3TeC4C7Q74/ryNced0TbDOTYBPX7xLlXb3aLLdqeyIraNluGVxpP1GTx2zQrgP7erwcN1jt02LaZMDaPH7RDfF/9ulyaqhxTIzMflsxf77c0JSP2LRMld15KVzwIxbrocKACt6hlw9PI8U0JKxGb7BylhLmyuYQq7Rl83lzES8d7md9U4Dwy6r/xlPWHqAYj3Nz8XhQ9PPvLyy/PBo4RNiUDR788/8errBYEyUG+/PUtjYxgbvVZPcJt6DVSwQE98eOd7hvQEV5gDgnos+8GD6afhgUR8cPYjNxbTXCC4r6NAxiHegyCIj85+DQ/5LQZp+v8M+d2alLUwjJDvj2FYzgQNXf8AXD2vrUvE87wAAAAASUVORK5CYII=" id="image8c6ddab5e9" transform="matrix(2.550857 0 0 2.550857 118.820571 242.941311)" style="image-rendering:crisp-edges;image-rendering:pixelated" width="20" height="20"/> - + +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" id="imaged4b3b72061" transform="scale(1 -1) translate(0 -51.12)" x="180.041143" y="-242.838454" width="51.12" height="51.12"/> - + +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" id="image2636f74bf5" transform="scale(1 -1) translate(0 -51.12)" x="241.261714" y="-242.838454" width="51.12" height="51.12"/> - + +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" id="imagef343df5550" transform="scale(1 -1) translate(0 -51.12)" x="302.482286" y="-242.838454" width="51.12" height="51.12"/> - + +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" id="image76dd97cf22" transform="scale(1 -1) translate(0 -51.12)" x="363.702857" y="-242.838454" width="51.12" height="51.12"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + diff --git a/lib/matplotlib/tests/baseline_images/test_image/interp_alpha.png b/lib/matplotlib/tests/baseline_images/test_image/interp_alpha.png index 5679a2f97df8..7129fba6d5b4 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/interp_alpha.png and b/lib/matplotlib/tests/baseline_images/test_image/interp_alpha.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.pdf b/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.pdf index c26419850251..3ff7ac577202 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.pdf and b/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.png b/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.png index 1df80c1b2045..453ea0ab2342 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.png and b/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.svg b/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.svg index 259eb2c9c7f3..e9bde5b99554 100644 --- a/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.svg +++ b/lib/matplotlib/tests/baseline_images/test_image/log_scale_image.svg @@ -6,11 +6,11 @@ - 2025-05-14T18:02:41.587512 + 2025-07-10T19:29:53.473547 image/svg+xml - Matplotlib v3.11.0.dev832+gc5ea66e278, https://matplotlib.org/ + Matplotlib v3.11.0.dev1075+g945334b731, https://matplotlib.org/ @@ -39,7 +39,7 @@ z +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" id="image2e672a6aec" transform="scale(1 -1) translate(0 -345.6)" x="72" y="-43.2" width="446.4" height="345.6"/> - - + + + + + + 2025-10-08T04:52:19.019436 + image/svg+xml + + + Matplotlib v3.11.0.dev1425+gb39ccbe8f, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,232 +35,252 @@ L 468 388.8 L 468 43.2 L 122.4 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - +" clip-path="url(#p31ed4989a5)" style="fill: none; stroke: #000000; stroke-width: 0.5; stroke-linecap: square"/> + + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - + - +" clip-path="url(#p31ed4989a5)" style="fill: none; stroke: #000000; stroke-width: 0.5; stroke-linecap: square"/> + + - + - + - + - + - + - +" clip-path="url(#p31ed4989a5)" style="fill: none; stroke: #000000; stroke-width: 0.5; stroke-linecap: square"/> + + - + - + - + - + - + - +" clip-path="url(#p31ed4989a5)" style="fill: none; stroke: #000000; stroke-width: 0.5; stroke-linecap: square"/> + + - + - + - + - + - + - +" clip-path="url(#p31ed4989a5)" style="fill: none; stroke: #000000; stroke-width: 0.5; stroke-linecap: square"/> + + - + - + - + - + - + - +" clip-path="url(#p31ed4989a5)" style="fill: none; stroke: #000000; stroke-width: 0.5; stroke-linecap: square"/> + + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - + - +" clip-path="url(#p31ed4989a5)" style="fill: none; stroke: #000000; stroke-width: 0.5; stroke-linecap: square"/> + + - + - + - + - + - + - +" clip-path="url(#p31ed4989a5)" style="fill: none; stroke: #000000; stroke-width: 0.5; stroke-linecap: square"/> + + - + - + - + - + - + - +" clip-path="url(#p31ed4989a5)" style="fill: none; stroke: #000000; stroke-width: 0.5; stroke-linecap: square"/> + + - + - + - + - + - + - +" clip-path="url(#p31ed4989a5)" style="fill: none; stroke: #000000; stroke-width: 0.5; stroke-linecap: square"/> + + - + - + - + @@ -257,20 +288,21 @@ L 468 388.8 +" style="fill: none; stroke: #ffffff; stroke-width: 5; stroke-linecap: round"/> +" style="fill: none; stroke: #000000; stroke-width: 2; stroke-linecap: round"/> +" style="fill: none; stroke: #ffffff; stroke-width: 5; stroke-linecap: round"/> - - + + + - + - - - - - - + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_png/pngsuite.png b/lib/matplotlib/tests/baseline_images/test_png/pngsuite.png index e8ba8c51be42..8b567e0a0598 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_png/pngsuite.png and b/lib/matplotlib/tests/baseline_images/test_png/pngsuite.png differ diff --git a/lib/matplotlib/tests/test__style_helpers.py b/lib/matplotlib/tests/test__style_helpers.py new file mode 100644 index 000000000000..764bd5a0c88e --- /dev/null +++ b/lib/matplotlib/tests/test__style_helpers.py @@ -0,0 +1,83 @@ +import pytest + +import matplotlib.colors as mcolors +from matplotlib.lines import _get_dash_pattern +from matplotlib._style_helpers import style_generator + + +@pytest.mark.parametrize('key, value', [('facecolor', ["b", "g", "r"]), + ('edgecolor', ["b", "g", "r"]), + ('hatch', ["/", "\\", "."]), + ('linestyle', ["-", "--", ":"]), + ('linewidth', [1, 1.5, 2])]) +def test_style_generator_list(key, value): + """Test that style parameter lists are distributed to the generator.""" + kw = {'foo': 12, key: value} + new_kw, gen = style_generator(kw) + + assert new_kw == {'foo': 12} + + for v in value * 2: # Result should repeat + style_dict = next(gen) + assert len(style_dict) == 1 + if key.endswith('color'): + assert mcolors.same_color(v, style_dict[key]) + elif key == 'linestyle': + assert _get_dash_pattern(v) == style_dict[key] + else: + assert v == style_dict[key] + + +@pytest.mark.parametrize('key, value', [('facecolor', "b"), + ('edgecolor', "b"), + ('hatch', "/"), + ('linestyle', "-"), + ('linewidth', 1)]) +def test_style_generator_single(key, value): + """Test that single-value style parameters are distributed to the generator.""" + kw = {'foo': 12, key: value} + new_kw, gen = style_generator(kw) + + assert new_kw == {'foo': 12} + for _ in range(2): # Result should repeat + style_dict = next(gen) + if key.endswith('color'): + assert mcolors.same_color(value, style_dict[key]) + elif key == 'linestyle': + assert _get_dash_pattern(value) == style_dict[key] + else: + assert value == style_dict[key] + + +@pytest.mark.parametrize('key', ['facecolor', 'hatch', 'linestyle']) +def test_style_generator_raises_on_empty_style_parameter_list(key): + kw = {key: []} + with pytest.raises(TypeError, match=f'{key} must not be an empty sequence'): + style_generator(kw) + + +def test_style_generator_sequence_type_styles(): + """ + Test that sequence type style values are detected as single value + and passed to a all elements of the generator. + """ + kw = {'facecolor': ('r', 0.5), + 'edgecolor': [0.5, 0.5, 0.5], + 'linestyle': (0, (1, 1))} + + _, gen = style_generator(kw) + for _ in range(2): # Result should repeat + style_dict = next(gen) + mcolors.same_color(kw['facecolor'], style_dict['facecolor']) + mcolors.same_color(kw['edgecolor'], style_dict['edgecolor']) + kw['linestyle'] == style_dict['linestyle'] + + +def test_style_generator_none(): + kw = {'facecolor': 'none', + 'edgecolor': 'none'} + _, gen = style_generator(kw) + for _ in range(2): # Result should repeat + style_dict = next(gen) + assert style_dict['facecolor'] == 'none' + assert style_dict['edgecolor'] == 'none' diff --git a/lib/matplotlib/tests/test_agg.py b/lib/matplotlib/tests/test_agg.py index 56b26904d041..69434fd393ec 100644 --- a/lib/matplotlib/tests/test_agg.py +++ b/lib/matplotlib/tests/test_agg.py @@ -263,6 +263,20 @@ def test_pil_kwargs_webp(): assert buf_large.getbuffer().nbytes > buf_small.getbuffer().nbytes +@pytest.mark.skipif(not features.check("avif"), reason="AVIF support not available") +def test_pil_kwargs_avif(): + plt.plot([0, 1, 2], [0, 1, 0]) + buf_small = io.BytesIO() + pil_kwargs_low = {"quality": 1} + plt.savefig(buf_small, format="avif", pil_kwargs=pil_kwargs_low) + assert len(pil_kwargs_low) == 1 + buf_large = io.BytesIO() + pil_kwargs_high = {"quality": 100} + plt.savefig(buf_large, format="avif", pil_kwargs=pil_kwargs_high) + assert len(pil_kwargs_high) == 1 + assert buf_large.getbuffer().nbytes > buf_small.getbuffer().nbytes + + def test_gif_no_alpha(): plt.plot([0, 1, 2], [0, 1, 0]) buf = io.BytesIO() @@ -290,6 +304,15 @@ def test_webp_alpha(): assert im.mode == "RGBA" +@pytest.mark.skipif(not features.check("avif"), reason="AVIF support not available") +def test_avif_alpha(): + plt.plot([0, 1, 2], [0, 1, 0]) + buf = io.BytesIO() + plt.savefig(buf, format="avif", transparent=True) + im = Image.open(buf) + assert im.mode == "RGBA" + + def test_draw_path_collection_error_handling(): fig, ax = plt.subplots() ax.scatter([1], [1]).set_paths(Path([(0, 1), (2, 3)])) @@ -351,7 +374,7 @@ def test_chunksize_fails(): def test_non_tuple_rgbaface(): - # This passes rgbaFace as a ndarray to draw_path. + # This passes rgbaFace as an ndarray to draw_path. fig = plt.figure() fig.add_subplot(projection="3d").scatter( [0, 1, 2], [0, 1, 2], path_effects=[patheffects.Stroke(linewidth=4)]) diff --git a/lib/matplotlib/tests/test_api.py b/lib/matplotlib/tests/test_api.py index f04604c14cce..4d0241264ddb 100644 --- a/lib/matplotlib/tests/test_api.py +++ b/lib/matplotlib/tests/test_api.py @@ -13,7 +13,7 @@ if typing.TYPE_CHECKING: - from typing_extensions import Self + from typing import Self T = TypeVar('T') @@ -150,3 +150,8 @@ def f() -> None: def test_empty_check_in_list() -> None: with pytest.raises(TypeError, match="No argument to check!"): _api.check_in_list(["a"]) + + +def test_check_in_list_numpy() -> None: + with pytest.raises(ValueError, match=r"array\(5\) is not a valid value"): + _api.check_in_list(['a', 'b'], value=np.array(5)) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index c96173e340f7..0c68a77db049 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -8,8 +8,10 @@ import io from itertools import product import platform +import re import sys from types import SimpleNamespace +import unittest.mock import dateutil.tz @@ -44,7 +46,6 @@ from matplotlib.testing.decorators import ( image_comparison, check_figures_equal, remove_ticks_and_titles) from matplotlib.testing._markers import needs_usetex - # Note: Some test cases are run twice: once normally and once with labeled data # These two must be defined in the same test function or need to have # different baseline images to prevent race conditions when pytest runs @@ -238,8 +239,9 @@ def test_matshow(fig_test, fig_ref): ax_ref.xaxis.set_ticks_position('both') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison([f'formatter_ticker_{i:03d}.png' for i in range(1, 6)], - tol=0 if platform.machine() == 'x86_64' else 0.031) + tol=0.02 if platform.machine() == 'x86_64' else 0.04) def test_formatter_ticker(): import matplotlib.testing.jpl_units as units units.register() @@ -808,7 +810,8 @@ def test_annotate_signature(): assert p1 == p2 -@image_comparison(['fill_units.png'], savefig_kwarg={'dpi': 60}) +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['fill_units.png'], savefig_kwarg={'dpi': 60}, tol=0.2) def test_fill_units(): import matplotlib.testing.jpl_units as units units.register() @@ -1382,7 +1385,8 @@ def test_pcolorargs_5205(): plt.pcolor(X, Y, list(Z[:-1, :-1])) -@image_comparison(['pcolormesh'], remove_text=True) +@image_comparison(['pcolormesh'], remove_text=True, + tol=0.11 if platform.machine() == 'aarch64' else 0) def test_pcolormesh(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -1434,7 +1438,7 @@ def test_pcolormesh_small(): @image_comparison(['pcolormesh_alpha'], extensions=["png", "pdf"], remove_text=True, - tol=0.2 if platform.machine() == "aarch64" else 0) + tol=0.4 if platform.machine() == "aarch64" else 0) def test_pcolormesh_alpha(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -1514,7 +1518,8 @@ def test_pcolormesh_log_scale(fig_test, fig_ref): ax.set_xscale('log') -@image_comparison(['pcolormesh_datetime_axis.png'], style='mpl20') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['pcolormesh_datetime_axis.png'], style='mpl20', tol=0.3) def test_pcolormesh_datetime_axis(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -1542,7 +1547,8 @@ def test_pcolormesh_datetime_axis(): label.set_rotation(30) -@image_comparison(['pcolor_datetime_axis.png'], style='mpl20') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['pcolor_datetime_axis.png'], style='mpl20', tol=0.3) def test_pcolor_datetime_axis(): fig = plt.figure() fig.subplots_adjust(hspace=0.4, top=0.98, bottom=.15) @@ -1574,6 +1580,9 @@ def test_pcolor_log_scale(fig_test, fig_ref): when using pcolor. """ x = np.linspace(0, 1, 11) + # Ensuring second x value always falls slightly above 0.1 prevents flakiness with + # numpy v1 #30882. This can be removed once we require numpy >= 2. + x[1] += 0.00001 y = np.linspace(1, 2, 5) X, Y = np.meshgrid(x, y) C = X[:-1, :-1] + Y[:-1, :-1] @@ -2266,6 +2275,20 @@ def test_grouped_bar_return_value(): assert bc not in ax.containers +def test_grouped_bar_hatch_sequence(): + """Each dataset should receive its own hatch pattern when a sequence is passed.""" + fig, ax = plt.subplots() + x = np.arange(2) + heights = [np.array([1, 2]), np.array([2, 3]), np.array([3, 4])] + hatches = ['//', 'xx', '..'] + containers = ax.grouped_bar(heights, positions=x, hatch=hatches) + + # Verify each dataset gets the corresponding hatch + for hatch, c in zip(hatches, containers.bar_containers): + for rect in c: + assert rect.get_hatch() == hatch + + def test_boxplot_dates_pandas(pd): # smoke test for boxplot and dates in pandas data = np.random.rand(5, 2) @@ -2564,6 +2587,15 @@ def test_hist_zorder(histtype, zorder): assert patch.get_zorder() == zorder +def test_hist_single_color_multiple_datasets(): + data = [[0, 1, 2], [3, 4, 5]] + _, _, bar_containers = plt.hist(data, color='k') + for p in bar_containers[0].patches: + assert mcolors.same_color(p.get_facecolor(), 'k') + for p in bar_containers[1].patches: + assert mcolors.same_color(p.get_facecolor(), 'k') + + def test_stairs_no_baseline_fill_warns(): fig, ax = plt.subplots() with pytest.warns(UserWarning, match="baseline=None and fill=True"): @@ -2737,7 +2769,8 @@ def test_stairs_options(): ax.legend(loc=0) -@image_comparison(['test_stairs_datetime.png']) +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['test_stairs_datetime.png'], tol=0.2) def test_stairs_datetime(): f, ax = plt.subplots(constrained_layout=True) ax.stairs(np.arange(36), @@ -2836,6 +2869,16 @@ def test_hist2d_density(): obj.hist2d(x, y, density=True) +@mpl.style.context("mpl20") +def test_hist2d_autolimits(): + x, y = np.random.random((2, 100)) + ax = plt.figure().add_subplot() + ax.hist2d(x, y) + assert ax.get_xlim() == (x.min(), x.max()) + assert ax.get_ylim() == (y.min(), y.max()) + assert ax.get_autoscale_on() # Autolimits have not been disabled. + + class TestScatter: @image_comparison(['scatter'], style='mpl20', remove_text=True) def test_scatter_plot(self): @@ -2946,11 +2989,11 @@ def test_scatter_unfillable(self): def test_scatter_size_arg_size(self): x = np.arange(4) - with pytest.raises(ValueError, match='same size as x and y'): + with pytest.raises(ValueError, match='cannot be broadcast to match x and y'): plt.scatter(x, x, x[1:]) - with pytest.raises(ValueError, match='same size as x and y'): + with pytest.raises(ValueError, match='cannot be broadcast to match x and y'): plt.scatter(x[1:], x[1:], x) - with pytest.raises(ValueError, match='float array-like'): + with pytest.raises(ValueError, match='must be float'): plt.scatter(x, x, 'foo') def test_scatter_edgecolor_RGB(self): @@ -2965,8 +3008,7 @@ def test_scatter_edgecolor_RGB(self): @check_figures_equal() def test_scatter_invalid_color(self, fig_test, fig_ref): ax = fig_test.subplots() - cmap = mpl.colormaps["viridis"].resampled(16) - cmap.set_bad("k", 1) + cmap = mpl.colormaps["viridis"].resampled(16).with_extremes(bad="black") # Set a nonuniform size to prevent the last call to `scatter` (plotting # the invalid points separately in fig_ref) from using the marker # stamping fast path, which would result in slightly offset markers. @@ -2982,8 +3024,7 @@ def test_scatter_invalid_color(self, fig_test, fig_ref): def test_scatter_no_invalid_color(self, fig_test, fig_ref): # With plotnonfinite=False we plot only 2 points. ax = fig_test.subplots() - cmap = mpl.colormaps["viridis"].resampled(16) - cmap.set_bad("k", 1) + cmap = mpl.colormaps["viridis"].resampled(16).with_extremes(bad="k") ax.scatter(range(4), range(4), c=[1, np.nan, 2, np.nan], s=[1, 2, 3, 4], cmap=cmap, plotnonfinite=False) @@ -3420,6 +3461,26 @@ def test_stackplot_hatching(fig_ref, fig_test): ax_ref.set_ylim(0, 70) +def test_stackplot_facecolor(): + # Test that facecolors are properly passed and take precedence over colors parameter + x = np.linspace(0, 10, 10) + y1 = 1.0 * x + y2 = 2.0 * x + 1 + + facecolors = ['r', 'b'] + + fig, ax = plt.subplots() + + colls = ax.stackplot(x, y1, y2, facecolor=facecolors, colors=['c', 'm']) + for coll, fcolor in zip(colls, facecolors): + assert mcolors.same_color(coll.get_facecolor(), fcolor) + + # Plural alias should also work + colls = ax.stackplot(x, y1, y2, facecolors=facecolors, colors=['c', 'm']) + for coll, fcolor in zip(colls, facecolors): + assert mcolors.same_color(coll.get_facecolor(), fcolor) + + def test_stackplot_subfig_legend(): # Smoke test for https://github.com/matplotlib/matplotlib/issues/30158 @@ -4172,6 +4233,10 @@ def color_violins(parts, facecolor=None, linecolor=None): if facecolor is not None: for pc in parts['bodies']: pc.set_facecolor(facecolor) + # disable alpha Artist property to counter the legacy behavior + # that applies an alpha of 0.3 to the bodies if no facecolor + # was set + pc.set_alpha(None) if linecolor is not None: for partname in ('cbars', 'cmins', 'cmaxes', 'cmeans', 'cmedians'): if partname in parts: @@ -4229,6 +4294,33 @@ def assert_colors_equal(colors1, colors2): assert_colors_equal(colors_test, mcolors.to_rgba_array(linecolors)) +def test_violinplot_alpha(): + matplotlib.style.use('default') + data = [(np.random.normal(0, 1, 100))] + + fig, ax = plt.subplots() + parts = ax.violinplot(data, positions=[1]) + + # Case 1: If facecolor is unspecified, it's the first color from the color cycle + # with Artist-level alpha=0.3 + facecolor = ('y' if mpl.rcParams['_internal.classic_mode'] + else plt.rcParams['axes.prop_cycle'].by_key()['color'][0]) + assert mcolors.same_color(parts['bodies'][0].get_facecolor(), (facecolor, 0.3)) + assert parts['bodies'][0].get_alpha() == 0.3 + # setting a new facecolor maintains the alpha + parts['bodies'][0].set_facecolor('red') + assert mcolors.same_color(parts['bodies'][0].get_facecolor(), ('red', 0.3)) + + # Case 2: If facecolor is explicitly given, it's alpha does not become an + # Artist property + parts = ax.violinplot(data, positions=[1], facecolor=('blue', 0.3)) + assert mcolors.same_color(parts['bodies'][0].get_facecolor(), ('blue', 0.3)) + assert parts['bodies'][0].get_alpha() is None + # so setting a new color does not maintain the alpha + parts['bodies'][0].set_facecolor('red') + assert mcolors.same_color(parts['bodies'][0].get_facecolor(), 'red') + + @check_figures_equal() def test_violinplot_single_list_quantiles(fig_test, fig_ref): # Ensures quantile list for 1D can be passed in as single list @@ -5005,27 +5097,6 @@ def test_hist_vectorized_params(fig_test, fig_ref, kwargs): zorder=(len(xs)-i)/2) -def test_hist_sequence_type_styles(): - facecolor = ('r', 0.5) - edgecolor = [0.5, 0.5, 0.5] - linestyle = (0, (1, 1)) - - arr = np.random.uniform(size=50) - _, _, bars = plt.hist(arr, facecolor=facecolor, edgecolor=edgecolor, - linestyle=linestyle) - assert mcolors.same_color(bars[0].get_facecolor(), facecolor) - assert mcolors.same_color(bars[0].get_edgecolor(), edgecolor) - assert bars[0].get_linestyle() == linestyle - - -def test_hist_color_none(): - arr = np.random.uniform(size=50) - # No edgecolor is the default but check that it can be explicitly passed. - _, _, bars = plt.hist(arr, facecolor='none', edgecolor='none') - assert bars[0].get_facecolor(), (0, 0, 0, 0) - assert bars[0].get_edgecolor(), (0, 0, 0, 0) - - @pytest.mark.parametrize('kwargs, patch_face, patch_edge', # 'C0'(blue) stands for the first color of the # default color cycle as well as the patch.facecolor rcParam @@ -6438,7 +6509,8 @@ def test_pie_frame_grid(): plt.axis('equal') -@image_comparison(['pie_rotatelabels_true.png'], style='mpl20', tol=0.009) +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['pie_rotatelabels_true.png'], style='mpl20', tol=0.1) def test_pie_rotatelabels_true(): # The slices will be ordered and plotted counter-clockwise. labels = 'Hogwarts', 'Frogs', 'Dogs', 'Logs' @@ -6568,6 +6640,57 @@ def test_pie_hatch_multi(fig_test, fig_ref): [w.set_hatch(hp) for w, hp in zip(wedges, hatch)] +def test_pie_label_formatter(): + fig, ax = plt.subplots() + pie = ax.pie([2, 3]) + + texts = ax.pie_label(pie, '{absval:03d}') + assert texts[0].get_text() == '002' + assert texts[1].get_text() == '003' + + texts = ax.pie_label(pie, '{frac:.1%}') + assert texts[0].get_text() == '40.0%' + assert texts[1].get_text() == '60.0%' + + +@pytest.mark.parametrize('distance', [0.6, 1.1]) +@pytest.mark.parametrize('rotate', [False, True]) +def test_pie_label_auto_align(distance, rotate): + fig, ax = plt.subplots() + pie = ax.pie([1, 1], startangle=45) + + texts = ax.pie_label( + pie, ['spam', 'eggs'], distance=distance, rotate=rotate, alignment='auto') + + if distance < 1: + for text in texts: + # labels within the pie should be centered + assert text.get_horizontalalignment() == 'center' + assert text.get_verticalalignment() == 'center' + + else: + # labels outside the pie should be aligned away from it + h_expected = ['right', 'left'] + v_expected = ['bottom', 'top'] + for text, h_align, v_align in zip(texts, h_expected, v_expected): + assert text.get_horizontalalignment() == h_align + if rotate: + assert text.get_verticalalignment() == v_align + else: + assert text.get_verticalalignment() == 'center' + + +def test_pie_label_fail(): + sizes = 15, 30, 45, 10 + labels = 'Frogs', 'Hogs' + fig, ax = plt.subplots() + pie = ax.pie(sizes) + + match = re.escape("The number of labels (2) must match the number of wedges (4)") + with pytest.raises(ValueError, match=match): + ax.pie_label(pie, labels) + + @image_comparison(['set_get_ticklabels.png'], tol=0 if platform.machine() == 'x86_64' else 0.025) def test_set_get_ticklabels(): @@ -7330,6 +7453,21 @@ def test_broken_barh_timedelta(): assert pp.get_paths()[0].vertices[2, 0] == mdates.date2num(d0) + 1 / 24 +def test_broken_barh_align(): + fig, ax = plt.subplots() + pc = ax.broken_barh([(0, 10)], (0, 2)) + for path in pc.get_paths(): + assert_array_equal(path.get_extents().intervaly, [0, 2]) + + pc = ax.broken_barh([(0, 10)], (10, 2), align="center") + for path in pc.get_paths(): + assert_array_equal(path.get_extents().intervaly, [9, 11]) + + pc = ax.broken_barh([(0, 10)], (20, 2), align="top") + for path in pc.get_paths(): + assert_array_equal(path.get_extents().intervaly, [18, 20]) + + def test_pandas_pcolormesh(pd): time = pd.date_range('2000-01-01', periods=10) depth = np.arange(20) @@ -8193,6 +8331,18 @@ def test_secondary_formatter(): secax.xaxis.get_major_formatter(), mticker.ScalarFormatter) +def test_secondary_init_xticks(): + fig, ax = plt.subplots() + secax = ax.secondary_xaxis(1, xticks=[0, 1]) + assert isinstance(secax.xaxis.get_major_locator(), mticker.FixedLocator) + with pytest.raises(TypeError): + secax.set_yticks([0, 1]) + secax = ax.secondary_yaxis(1, yticks=[0, 1]) + assert isinstance(secax.yaxis.get_major_locator(), mticker.FixedLocator) + with pytest.raises(TypeError): + secax.set_xticks([0, 1]) + + def test_secondary_repr(): fig, ax = plt.subplots() secax = ax.secondary_xaxis("top") @@ -8223,10 +8373,9 @@ def color_boxes(fig, ax): """ fig.canvas.draw() - renderer = fig.canvas.get_renderer() bbaxis = [] for nn, axx in enumerate([ax.xaxis, ax.yaxis]): - bb = axx.get_tightbbox(renderer) + bb = axx.get_tightbbox() if bb: axisr = mpatches.Rectangle( (bb.x0, bb.y0), width=bb.width, height=bb.height, @@ -8237,7 +8386,7 @@ def color_boxes(fig, ax): bbspines = [] for nn, a in enumerate(['bottom', 'top', 'left', 'right']): - bb = ax.spines[a].get_window_extent(renderer) + bb = ax.spines[a].get_window_extent() spiner = mpatches.Rectangle( (bb.x0, bb.y0), width=bb.width, height=bb.height, linewidth=0.7, edgecolor="green", facecolor="none", transform=None, @@ -8253,7 +8402,7 @@ def color_boxes(fig, ax): fig.add_artist(rect2) bbax = bb - bb2 = ax.get_tightbbox(renderer) + bb2 = ax.get_tightbbox() rect2 = mpatches.Rectangle( (bb2.x0, bb2.y0), width=bb2.width, height=bb2.height, linewidth=3, edgecolor="red", facecolor="none", transform=None, @@ -9038,7 +9187,7 @@ def test_patch_bounds(): # PR 19078 @mpl.style.context('default') def test_warn_ignored_scatter_kwargs(): with pytest.warns(UserWarning, - match=r"You passed a edgecolor/edgecolors"): + match=r"You passed an edgecolor/edgecolors"): plt.scatter([0], [0], marker="+", s=500, facecolor="r", edgecolor="b") @@ -9900,3 +10049,20 @@ def test_pie_all_zeros(): fig, ax = plt.subplots() with pytest.raises(ValueError, match="All wedge sizes are zero"): ax.pie([0, 0], labels=["A", "B"]) + + +def test_animated_artists_not_drawn_by_default(): + fig, (ax1, ax2) = plt.subplots(ncols=2) + + imdata = np.random.random((20, 20)) + lndata = imdata[0] + + im = ax1.imshow(imdata, animated=True) + (ln,) = ax2.plot(lndata, animated=True) + + with (unittest.mock.patch.object(im, "draw", name="im.draw") as mocked_im_draw, + unittest.mock.patch.object(ln, "draw", name="ln.draw") as mocked_ln_draw): + fig.draw_without_rendering() + + mocked_im_draw.assert_not_called() + mocked_ln_draw.assert_not_called() diff --git a/lib/matplotlib/tests/test_axis.py b/lib/matplotlib/tests/test_axis.py index 97884a33208f..67d9ed5bde62 100644 --- a/lib/matplotlib/tests/test_axis.py +++ b/lib/matplotlib/tests/test_axis.py @@ -2,6 +2,7 @@ import matplotlib.pyplot as plt from matplotlib.axis import XTick +from matplotlib.testing.decorators import check_figures_equal def test_tick_labelcolor_array(): @@ -31,6 +32,21 @@ def test_axis_not_in_layout(): assert ax1_right.get_position().bounds == ax2_right.get_position().bounds +@check_figures_equal() +def test_tick_not_in_layout(fig_test, fig_ref): + # Check that the "very long" ticklabel is ignored from layouting after + # set_in_layout(False); i.e. the layout is as if the ticklabel was empty. + # Ticklabels are set to white so that the actual string doesn't matter. + fig_test.set_layout_engine("constrained") + ax = fig_test.add_subplot(xticks=[0, 1], xticklabels=["short", "very long"]) + ax.tick_params(labelcolor="w") + fig_test.draw_without_rendering() # Ensure ticks are correct. + ax.xaxis.majorTicks[-1].label1.set_in_layout(False) + fig_ref.set_layout_engine("constrained") + ax = fig_ref.add_subplot(xticks=[0, 1], xticklabels=["short", ""]) + ax.tick_params(labelcolor="w") + + def test_translate_tick_params_reverse(): fig, ax = plt.subplots() kw = {'label1On': 'a', 'label2On': 'b', 'tick1On': 'c', 'tick2On': 'd'} diff --git a/lib/matplotlib/tests/test_backend_gtk3.py b/lib/matplotlib/tests/test_backend_gtk3.py index b4c6e3d7fca8..a299d21a4b7b 100644 --- a/lib/matplotlib/tests/test_backend_gtk3.py +++ b/lib/matplotlib/tests/test_backend_gtk3.py @@ -5,51 +5,6 @@ from unittest import mock -@pytest.mark.backend("gtk3agg", skip_on_importerror=True) -def test_correct_key(): - pytest.xfail("test_widget_send_event is not triggering key_press_event") - - from gi.repository import Gdk, Gtk # type: ignore[import] - fig = plt.figure() - buf = [] - - def send(event): - for key, mod in [ - (Gdk.KEY_a, Gdk.ModifierType.SHIFT_MASK), - (Gdk.KEY_a, 0), - (Gdk.KEY_a, Gdk.ModifierType.CONTROL_MASK), - (Gdk.KEY_agrave, 0), - (Gdk.KEY_Control_L, Gdk.ModifierType.MOD1_MASK), - (Gdk.KEY_Alt_L, Gdk.ModifierType.CONTROL_MASK), - (Gdk.KEY_agrave, - Gdk.ModifierType.CONTROL_MASK - | Gdk.ModifierType.MOD1_MASK - | Gdk.ModifierType.MOD4_MASK), - (0xfd16, 0), # KEY_3270_Play. - (Gdk.KEY_BackSpace, 0), - (Gdk.KEY_BackSpace, Gdk.ModifierType.CONTROL_MASK), - ]: - # This is not actually really the right API: it depends on the - # actual keymap (e.g. on Azerty, shift+agrave -> 0). - Gtk.test_widget_send_key(fig.canvas, key, mod) - - def receive(event): - buf.append(event.key) - if buf == [ - "A", "a", "ctrl+a", - "\N{LATIN SMALL LETTER A WITH GRAVE}", - "alt+control", "ctrl+alt", - "ctrl+alt+super+\N{LATIN SMALL LETTER A WITH GRAVE}", - # (No entry for KEY_3270_Play.) - "backspace", "ctrl+backspace", - ]: - plt.close(fig) - - fig.canvas.mpl_connect("draw_event", send) - fig.canvas.mpl_connect("key_press_event", receive) - plt.show() - - @pytest.mark.backend("gtk3agg", skip_on_importerror=True) def test_save_figure_return(): from gi.repository import Gtk diff --git a/lib/matplotlib/tests/test_backend_macosx.py b/lib/matplotlib/tests/test_backend_macosx.py index fe4c9a6fba3c..0648e43cde94 100644 --- a/lib/matplotlib/tests/test_backend_macosx.py +++ b/lib/matplotlib/tests/test_backend_macosx.py @@ -1,4 +1,5 @@ import os +import threading from pathlib import Path import pytest @@ -84,3 +85,25 @@ def _test_save_figure_return(): def test_save_figure_return(): subprocess_run_helper(_test_save_figure_return, timeout=_test_timeout, extra_env={"MPLBACKEND": "macosx"}) + + +def _test_create_figure_on_worker_thread_fails(): + def create_figure(): + warn_msg = "Matplotlib GUI outside of the main thread will likely fail." + err_msg = "Cannot create a GUI FigureManager outside the main thread" + with pytest.warns(UserWarning, match=warn_msg): + with pytest.raises(RuntimeError, match=err_msg): + plt.gcf() + + worker = threading.Thread(target=create_figure) + worker.start() + worker.join() + + +@pytest.mark.backend('macosx', skip_on_importerror=True) +def test_create_figure_on_worker_thread_fails(): + subprocess_run_helper( + _test_create_figure_on_worker_thread_fails, + timeout=_test_timeout, + extra_env={"MPLBACKEND": "macosx"} + ) diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index f5ec85005079..9859a286e5fd 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -354,7 +354,11 @@ def test_path_collection(): sizes = [0.02, 0.04] pc = mcollections.PathCollection(paths, sizes, zorder=-1, facecolors='yellow', offsets=offsets) - ax.add_collection(pc) + # Note: autolim=False is used to keep the view limits as is for now, + # given the updated behavior of autolim=True to also update the view + # limits. It may be reasonable to test the limits handling in the future + # as well. This will require regenerating the reference image. + ax.add_collection(pc, autolim=False) ax.set_xlim(0, 1) diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index a17e98d70484..fda0f978ea02 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -15,6 +15,7 @@ from matplotlib import _c_internal_utils try: + from matplotlib.backends.qt_compat import QtCore # type: ignore[attr-defined] from matplotlib.backends.qt_compat import QtGui # type: ignore[attr-defined] # noqa: E501, F401 from matplotlib.backends.qt_compat import QtWidgets # type: ignore[attr-defined] from matplotlib.backends.qt_editor import _formlayout @@ -25,12 +26,6 @@ _test_timeout = 60 # A reasonably safe value for slower architectures. -@pytest.fixture -def qt_core(request): - from matplotlib.backends.qt_compat import QtCore - return QtCore - - @pytest.mark.backend('QtAgg', skip_on_importerror=True) def test_fig_close(): @@ -101,7 +96,7 @@ def test_fig_close(): 'QtAgg', marks=pytest.mark.backend('QtAgg', skip_on_importerror=True)), ]) -def test_correct_key(backend, qt_core, qt_key, qt_mods, answer, monkeypatch): +def test_correct_key(backend, qt_key, qt_mods, answer, monkeypatch): """ Make a figure. Send a key_press_event event (using non-public, qtX backend specific api). @@ -154,11 +149,18 @@ def test_device_pixel_ratio_change(): def set_device_pixel_ratio(ratio): p.return_value = ratio - # The value here doesn't matter, as we can't mock the C++ QScreen - # object, but can override the functional wrapper around it. - # Emitting this event is simply to trigger the DPI change handler - # in Matplotlib in the same manner that it would occur normally. - screen.logicalDotsPerInchChanged.emit(96) + window = qt_canvas.window().windowHandle() + current_version = tuple(int(x) for x in QtCore.qVersion().split('.', 2)[:2]) + if current_version >= (6, 6): + QtCore.QCoreApplication.sendEvent( + window, + QtCore.QEvent(QtCore.QEvent.Type.DevicePixelRatioChange)) + else: + # The value here doesn't matter, as we can't mock the C++ QScreen + # object, but can override the functional wrapper around it. + # Emitting this event is simply to trigger the DPI change handler + # in Matplotlib in the same manner that it would occur normally. + window.screen().logicalDotsPerInchChanged.emit(96) qt_canvas.draw() qt_canvas.flush_events() @@ -167,46 +169,34 @@ def set_device_pixel_ratio(ratio): assert qt_canvas.device_pixel_ratio == ratio qt_canvas.manager.show() + qt_canvas.draw() + qt_canvas.flush_events() size = qt_canvas.size() - screen = qt_canvas.window().windowHandle().screen() - set_device_pixel_ratio(3) - - # The DPI and the renderer width/height change - assert fig.dpi == 360 - assert qt_canvas.renderer.width == 1800 - assert qt_canvas.renderer.height == 720 - - # The actual widget size and figure logical size don't change. - assert size.width() == 600 - assert size.height() == 240 - assert qt_canvas.get_width_height() == (600, 240) - assert (fig.get_size_inches() == (5, 2)).all() - - set_device_pixel_ratio(2) - - # The DPI and the renderer width/height change - assert fig.dpi == 240 - assert qt_canvas.renderer.width == 1200 - assert qt_canvas.renderer.height == 480 - # The actual widget size and figure logical size don't change. - assert size.width() == 600 - assert size.height() == 240 - assert qt_canvas.get_width_height() == (600, 240) - assert (fig.get_size_inches() == (5, 2)).all() + options = [ + (None, 360, 1800, 720), # Use ratio at startup time. + (3, 360, 1800, 720), # Change to same ratio. + (2, 240, 1200, 480), # Change to different ratio. + (1.5, 180, 900, 360), # Fractional ratio. + ] + for ratio, dpi, width, height in options: + if ratio is not None: + set_device_pixel_ratio(ratio) - set_device_pixel_ratio(1.5) + # The DPI and the renderer width/height change + assert fig.dpi == dpi + assert qt_canvas.renderer.width == width + assert qt_canvas.renderer.height == height - # The DPI and the renderer width/height change - assert fig.dpi == 180 - assert qt_canvas.renderer.width == 900 - assert qt_canvas.renderer.height == 360 + # The actual widget size and figure logical size don't change. + assert size.width() == 600 + assert size.height() == 240 + assert qt_canvas.get_width_height() == (600, 240) + assert (fig.get_size_inches() == (5, 2)).all() - # The actual widget size and figure logical size don't change. - assert size.width() == 600 - assert size.height() == 240 - assert qt_canvas.get_width_height() == (600, 240) - assert (fig.get_size_inches() == (5, 2)).all() + # check that closing the figure restores the original dpi + plt.close(fig) + assert fig.dpi == 120 @pytest.mark.backend('QtAgg', skip_on_importerror=True) @@ -229,14 +219,15 @@ def test_figureoptions(): @pytest.mark.backend('QtAgg', skip_on_importerror=True) -def test_save_figure_return(): +def test_save_figure_return(tmp_path): fig, ax = plt.subplots() ax.imshow([[1]]) + expected = tmp_path / "foobar.png" prop = "matplotlib.backends.qt_compat.QtWidgets.QFileDialog.getSaveFileName" - with mock.patch(prop, return_value=("foobar.png", None)): + with mock.patch(prop, return_value=(str(expected), None)): fname = fig.canvas.manager.toolbar.save_figure() - os.remove("foobar.png") - assert fname == "foobar.png" + assert fname == str(expected) + assert expected.exists() with mock.patch(prop, return_value=(None, None)): fname = fig.canvas.manager.toolbar.save_figure() assert fname is None @@ -335,7 +326,7 @@ def _get_testable_qt_backends(): @pytest.mark.backend('QtAgg', skip_on_importerror=True) -def test_fig_sigint_override(qt_core): +def test_fig_sigint_override(): from matplotlib.backends.backend_qt5 import _BackendQT5 # Create a figure plt.figure() @@ -350,10 +341,10 @@ def fire_signal_and_quit(): event_loop_handler = signal.getsignal(signal.SIGINT) # Request event loop exit - qt_core.QCoreApplication.exit() + QtCore.QCoreApplication.exit() # Timer to exit event loop - qt_core.QTimer.singleShot(0, fire_signal_and_quit) + QtCore.QTimer.singleShot(0, fire_signal_and_quit) # Save original SIGINT handler original_handler = signal.getsignal(signal.SIGINT) @@ -378,7 +369,7 @@ def custom_handler(signum, frame): # Repeat again to test that SIG_DFL and SIG_IGN will not be overridden for custom_handler in (signal.SIG_DFL, signal.SIG_IGN): - qt_core.QTimer.singleShot(0, fire_signal_and_quit) + QtCore.QTimer.singleShot(0, fire_signal_and_quit) signal.signal(signal.SIGINT, custom_handler) _BackendQT5.mainloop() diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index 2c64b7c24b3e..7864b3bb68bd 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -74,7 +74,8 @@ def test_bold_font_output(): ax.plot(np.arange(10), np.arange(10)) ax.set_xlabel('nonbold-xlabel') ax.set_ylabel('bold-ylabel', fontweight='bold') - ax.set_title('bold-title', fontweight='bold') + # set weight as integer to assert it's handled properly + ax.set_title('bold-title', fontweight=600) @image_comparison(['bold_font_output_with_none_fonttype.svg']) @@ -84,7 +85,8 @@ def test_bold_font_output_with_none_fonttype(): ax.plot(np.arange(10), np.arange(10)) ax.set_xlabel('nonbold-xlabel') ax.set_ylabel('bold-ylabel', fontweight='bold') - ax.set_title('bold-title', fontweight='bold') + # set weight as integer to assert it's handled properly + ax.set_title('bold-title', fontweight=600) @check_figures_equal(extensions=['svg'], tol=20) diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index 9f8522a9df4a..101c1cb81cad 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -107,13 +107,7 @@ def _get_available_interactive_backends(): elif env["MPLBACKEND"].startswith('wx') and sys.platform == 'darwin': # ignore on macosx because that's currently broken (github #16849) marks.append(pytest.mark.xfail(reason='github #16849')) - elif (env['MPLBACKEND'] == 'tkagg' and - ('TF_BUILD' in os.environ or 'GITHUB_ACTION' in os.environ) and - sys.platform == 'darwin' and - sys.version_info[:2] < (3, 11) - ): - marks.append( # https://github.com/actions/setup-python/issues/649 - pytest.mark.xfail(reason='Tk version mismatch on Azure macOS CI')) + envs.append(({**env, 'BACKEND_DEPS': ','.join(deps)}, marks)) return envs @@ -127,6 +121,7 @@ def _get_testable_interactive_backends(): # Reasonable safe values for slower CI/Remote and local architectures. _test_timeout = 120 if is_ci_environment() else 20 +_retry_count = 3 if is_ci_environment() else 0 def _test_toolbar_button_la_mode_icon(fig): @@ -162,7 +157,7 @@ def _test_interactive_impl(): import matplotlib as mpl from matplotlib import pyplot as plt - from matplotlib.backend_bases import KeyEvent + from matplotlib.backend_bases import KeyEvent, FigureCanvasBase mpl.rcParams.update({ "webagg.open_in_browser": False, "webagg.port_retries": 1, @@ -213,6 +208,10 @@ def check_alt_backend(alt_backend): if fig.canvas.toolbar: # i.e toolbar2. fig.canvas.toolbar.draw_rubberband(None, 1., 1, 2., 2) + if backend == 'webagg' and sys.version_info >= (3, 14): + import asyncio + asyncio.set_event_loop(asyncio.new_event_loop()) + timer = fig.canvas.new_timer(1.) # Test that floats are cast to int. timer.add_callback(KeyEvent("key_press_event", fig.canvas, "q")._process) # Trigger quitting upon draw. @@ -220,24 +219,28 @@ def check_alt_backend(alt_backend): fig.canvas.mpl_connect("close_event", print) result = io.BytesIO() - fig.savefig(result, format='png') + fig.savefig(result, format='png', dpi=100) plt.show() # Ensure that the window is really closed. plt.pause(0.5) - # Test that saving works after interactive window is closed, but the figure - # is not deleted. + # When the figure is closed, its manager is removed and the canvas is reset to + # FigureCanvasBase. Saving should still be possible. + assert type(fig.canvas) == FigureCanvasBase, str(fig.canvas) result_after = io.BytesIO() - fig.savefig(result_after, format='png') + fig.savefig(result_after, format='png', dpi=100) - assert result.getvalue() == result_after.getvalue() + if backend.endswith("agg"): + # agg-based interactive backends should save the same image as a non-interactive + # figure + assert result.getvalue() == result_after.getvalue() @pytest.mark.parametrize("env", _get_testable_interactive_backends()) @pytest.mark.parametrize("toolbar", ["toolbar2", "toolmanager"]) -@pytest.mark.flaky(reruns=3) +@pytest.mark.flaky(reruns=_retry_count) def test_interactive_backend(env, toolbar): if env["MPLBACKEND"] == "macosx": if toolbar == "toolmanager": @@ -285,10 +288,13 @@ def _test_thread_impl(): future = ThreadPoolExecutor().submit(fig.canvas.draw) plt.pause(0.5) # flush_events fails here on at least Tkagg (bpo-41176) future.result() # Joins the thread; rethrows any exception. + # stash the current canvas as closing the figure will reset the canvas on + # the figure + canvas = fig.canvas plt.close() # backend is responsible for flushing any events here if plt.rcParams["backend"].lower().startswith("wx"): # TODO: debug why WX needs this only on py >= 3.8 - fig.canvas.flush_events() + canvas.flush_events() _thread_safe_backends = _get_testable_interactive_backends() @@ -329,7 +335,7 @@ def _test_thread_impl(): @pytest.mark.parametrize("env", _thread_safe_backends) -@pytest.mark.flaky(reruns=3) +@pytest.mark.flaky(reruns=_retry_count) def test_interactive_thread_safety(env): proc = _run_helper(_test_thread_impl, timeout=_test_timeout, extra_env=env) assert proc.stdout.count("CloseEvent") == 1 @@ -617,7 +623,7 @@ def _test_number_of_draws_script(): @pytest.mark.parametrize("env", _blit_backends) # subprocesses can struggle to get the display, so rerun a few times -@pytest.mark.flaky(reruns=4) +@pytest.mark.flaky(reruns=_retry_count) def test_blitting_events(env): proc = _run_helper( _test_number_of_draws_script, timeout=_test_timeout, extra_env=env) diff --git a/lib/matplotlib/tests/test_bbox_tight.py b/lib/matplotlib/tests/test_bbox_tight.py index 431ca70bf7ea..9d430b78d5de 100644 --- a/lib/matplotlib/tests/test_bbox_tight.py +++ b/lib/matplotlib/tests/test_bbox_tight.py @@ -110,7 +110,7 @@ def test_bbox_inches_tight_clipping(): plt.gcf().artists.append(patch) -@image_comparison(['bbox_inches_tight_raster'], +@image_comparison(['bbox_inches_tight_raster'], tol=0.15, # For Ghostscript 10.06+. remove_text=True, savefig_kwarg={'bbox_inches': 'tight'}) def test_bbox_inches_tight_raster(): """Test rasterization with tight_layout""" diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 642e5829a7b5..c062e8c12b9c 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -408,7 +408,6 @@ def test_EllipseCollection(): ww, hh, aa, units='x', offsets=XY, offset_transform=ax.transData, facecolors='none') ax.add_collection(ec) - ax.autoscale_view() def test_EllipseCollection_setter_getter(): @@ -525,8 +524,7 @@ def test_regularpolycollection_rotate(): col = mcollections.RegularPolyCollection( 4, sizes=(100,), rotation=alpha, offsets=[xy], offset_transform=ax.transData) - ax.add_collection(col, autolim=True) - ax.autoscale_view() + ax.add_collection(col) @image_comparison(['regularpolycollection_scale.png'], remove_text=True) @@ -554,7 +552,7 @@ def get_transform(self): circle_areas = [np.pi / 2] squares = SquareCollection( sizes=circle_areas, offsets=xy, offset_transform=ax.transData) - ax.add_collection(squares, autolim=True) + ax.add_collection(squares) ax.axis([-1, 1, -1, 1]) @@ -899,17 +897,24 @@ def test_collection_set_array(): def test_blended_collection_autolim(): - a = [1, 2, 4] - height = .2 + f, ax = plt.subplots() - xy_pairs = np.column_stack([np.repeat(a, 2), np.tile([0, height], len(a))]) - line_segs = xy_pairs.reshape([len(a), 2, 2]) + # sample data to give initial data limits + ax.plot([2, 3, 4], [0.4, 0.6, 0.5]) + np.testing.assert_allclose((ax.dataLim.xmin, ax.dataLim.xmax), (2, 4)) + data_ymin, data_ymax = ax.dataLim.ymin, ax.dataLim.ymax - f, ax = plt.subplots() + # LineCollection with vertical lines spanning the Axes vertical, using transAxes + x = [1, 2, 3, 4, 5] + vertical_lines = [np.array([[xi, 0], [xi, 1]]) for xi in x] trans = mtransforms.blended_transform_factory(ax.transData, ax.transAxes) - ax.add_collection(LineCollection(line_segs, transform=trans)) - ax.autoscale_view(scalex=True, scaley=False) - np.testing.assert_allclose(ax.get_xlim(), [1., 4.]) + ax.add_collection(LineCollection(vertical_lines, transform=trans)) + + # check that the x data limits are updated to include the LineCollection + np.testing.assert_allclose((ax.dataLim.xmin, ax.dataLim.xmax), (1, 5)) + # check that the y data limits are not updated (because they are not transData) + np.testing.assert_allclose((ax.dataLim.ymin, ax.dataLim.ymax), + (data_ymin, data_ymax)) def test_singleton_autolim(): diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index ba20f325f4d7..72e38d32e82f 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -152,6 +152,7 @@ def test_colorbar_extension_inverted_axis(orientation, extend, expected): assert len(cbar._extend_patches) == 1 +# TODO: tighten tolerance after baseline image is regenerated for text overhaul @pytest.mark.parametrize('use_gridspec', [True, False]) @image_comparison(['cbar_with_orientation', 'cbar_locationing', @@ -159,7 +160,7 @@ def test_colorbar_extension_inverted_axis(orientation, extend, expected): 'cbar_sharing', ], extensions=['png'], remove_text=True, - savefig_kwarg={'dpi': 40}) + savefig_kwarg={'dpi': 40}, tol=0.05) def test_colorbar_positioning(use_gridspec): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -731,7 +732,8 @@ def test_colorbar_label(): assert cbar3.ax.get_xlabel() == 'horizontal cbar' -@image_comparison(['colorbar_keeping_xlabel.png'], style='mpl20') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['colorbar_keeping_xlabel.png'], style='mpl20', tol=0.03) def test_keeping_xlabel(): # github issue #23398 - xlabels being ignored in colorbar axis arr = np.arange(25).reshape((5, 5)) @@ -946,9 +948,10 @@ def test_proportional_colorbars(): levels = [-1.25, -0.5, -0.125, 0.125, 0.5, 1.25] cmap = mcolors.ListedColormap( - ['0.3', '0.5', 'white', 'lightblue', 'steelblue']) - cmap.set_under('darkred') - cmap.set_over('crimson') + ['0.3', '0.5', 'white', 'lightblue', 'steelblue'], + under='darkred', + over='crimson', + ) norm = mcolors.BoundaryNorm(levels, cmap.N) extends = ['neither', 'both'] diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 73de50408401..985a7b024ed4 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -9,6 +9,7 @@ import base64 import platform +from numpy.lib import recfunctions as rfn from numpy.testing import assert_array_equal, assert_array_almost_equal from matplotlib import cbook, cm @@ -52,13 +53,9 @@ def test_resampled(): colorlist[:, 1] = 0.2 colorlist[:, 2] = np.linspace(1, 0, n) colorlist[:, 3] = 0.7 - lsc = mcolors.LinearSegmentedColormap.from_list('lsc', colorlist) - lc = mcolors.ListedColormap(colorlist) - # Set some bad values for testing too - for cmap in [lsc, lc]: - cmap.set_under('r') - cmap.set_over('g') - cmap.set_bad('b') + lsc = mcolors.LinearSegmentedColormap.from_list( + 'lsc', colorlist, under='red', over='green', bad='blue') + lc = mcolors.ListedColormap(colorlist, under='red', over='green', bad='blue') lsc3 = lsc.resampled(3) lc3 = lc.resampled(3) expected = np.array([[0.0, 0.2, 1.0, 0.7], @@ -114,7 +111,8 @@ def test_colormap_copy(): with np.errstate(invalid='ignore'): ret1 = copied_cmap([-1, 0, .5, 1, np.nan, np.inf]) cmap2 = copy.copy(copied_cmap) - cmap2.set_bad('g') + with pytest.warns(PendingDeprecationWarning): + cmap2.set_bad('g') with np.errstate(invalid='ignore'): ret2 = copied_cmap([-1, 0, .5, 1, np.nan, np.inf]) assert_array_equal(ret1, ret2) @@ -124,7 +122,8 @@ def test_colormap_copy(): with np.errstate(invalid='ignore'): ret1 = copied_cmap([-1, 0, .5, 1, np.nan, np.inf]) cmap2 = copy.copy(copied_cmap) - cmap2.set_bad('g') + with pytest.warns(PendingDeprecationWarning): + cmap2.set_bad('g') with np.errstate(invalid='ignore'): ret2 = copied_cmap([-1, 0, .5, 1, np.nan, np.inf]) assert_array_equal(ret1, ret2) @@ -138,7 +137,8 @@ def test_colormap_equals(): # But the same data should be equal assert cm_copy == cmap # Change the copy - cm_copy.set_bad('y') + with pytest.warns(PendingDeprecationWarning): + cm_copy.set_bad('y') assert cm_copy != cmap # Make sure we can compare different sizes without failure cm_copy._lut = cm_copy._lut[:10, :] @@ -370,9 +370,7 @@ def test_BoundaryNorm(): assert_array_equal(mynorm(x), ref) # Without interpolation - cmref = mcolors.ListedColormap(['blue', 'red']) - cmref.set_over('black') - cmref.set_under('white') + cmref = mcolors.ListedColormap(['blue', 'red'], under='white', over='black') cmshould = mcolors.ListedColormap(['white', 'blue', 'red', 'black']) assert mcolors.same_color(cmref.get_over(), 'black') @@ -394,8 +392,7 @@ def test_BoundaryNorm(): assert_array_equal(cmshould(mynorm(x)), cmref(refnorm(x))) # Just min - cmref = mcolors.ListedColormap(['blue', 'red']) - cmref.set_under('white') + cmref = mcolors.ListedColormap(['blue', 'red'], under='white') cmshould = mcolors.ListedColormap(['white', 'blue', 'red']) assert mcolors.same_color(cmref.get_under(), 'white') @@ -412,8 +409,7 @@ def test_BoundaryNorm(): assert_array_equal(cmshould(mynorm(x)), cmref(refnorm(x))) # Just max - cmref = mcolors.ListedColormap(['blue', 'red']) - cmref.set_over('black') + cmref = mcolors.ListedColormap(['blue', 'red'], over='black') cmshould = mcolors.ListedColormap(['blue', 'red', 'black']) assert mcolors.same_color(cmref.get_over(), 'black') @@ -927,7 +923,7 @@ def test_cmap_and_norm_from_levels_and_colors2(): for extend, i1, cases in tests: cmap, norm = mcolors.from_levels_and_colors(levels, colors[0:i1], extend=extend) - cmap.set_bad(bad) + cmap = cmap.with_extremes(bad=bad) for d_val, expected_color in cases.items(): if d_val == masked_value: d_val = np.ma.array([1], mask=True) @@ -951,6 +947,11 @@ def test_rgb_hsv_round_trip(): tt, mcolors.rgb_to_hsv(mcolors.hsv_to_rgb(tt))) +def test_rgb_to_hsv_int(): + # Test that int rgb values (still range 0-1) are processed correctly. + assert_array_equal(mcolors.rgb_to_hsv((0, 1, 0)), (1/3, 1, 1)) # green + + def test_autoscale_masked(): # Test for #2336. Previously fully masked data would trigger a ValueError. data = np.ma.masked_all((12, 20)) @@ -1429,7 +1430,7 @@ def test_scalarmappable_nan_to_rgba(bytes): # Out-of-range fail x[1, 0, 0] = 42 - with pytest.raises(ValueError, match='0..1 range'): + with pytest.raises(ValueError, match=r'\[0,1\] range'): sm.to_rgba(x[..., :3], bytes=bytes) @@ -1542,7 +1543,8 @@ def test_get_under_over_bad(): def test_non_mutable_get_values(kind): cmap = copy.copy(mpl.colormaps['viridis']) init_value = getattr(cmap, f'get_{kind}')() - getattr(cmap, f'set_{kind}')('k') + with pytest.warns(PendingDeprecationWarning): + getattr(cmap, f'set_{kind}')('k') black_value = getattr(cmap, f'get_{kind}')() assert np.all(black_value == [0, 0, 0, 1]) assert not np.all(init_value == black_value) @@ -1705,8 +1707,8 @@ def test_color_sequences(): assert plt.color_sequences is matplotlib.color_sequences # same registry assert list(plt.color_sequences) == [ 'tab10', 'tab20', 'tab20b', 'tab20c', 'Pastel1', 'Pastel2', 'Paired', - 'Accent', 'Dark2', 'Set1', 'Set2', 'Set3', 'petroff6', 'petroff8', - 'petroff10'] + 'Accent', 'okabe_ito', 'Dark2', 'Set1', 'Set2', 'Set3', 'petroff6', + 'petroff8', 'petroff10'] assert len(plt.color_sequences['tab10']) == 10 assert len(plt.color_sequences['tab20']) == 20 @@ -1867,6 +1869,10 @@ def autoscale_None(self, A): def scaled(self): return True + @property + def n_components(self): + return 1 + fig, axes = plt.subplots(2,2) r = np.linspace(-1, 3, 16*16).reshape((16,16)) @@ -1876,3 +1882,369 @@ def scaled(self): axes[0,1].pcolor(r, colorizer=colorizer) axes[1,0].contour(r, colorizer=colorizer) axes[1,1].contourf(r, colorizer=colorizer) + + +def test_close_error_name(): + with pytest.raises( + KeyError, + match=( + "'grays' is not a valid value for colormap. " + "Did you mean one of ['gray', 'Grays', 'gray_r']?" + )): + matplotlib.colormaps["grays"] + + +def test_multi_norm_creation(): + # tests for mcolors.MultiNorm + + # test wrong input + with pytest.raises(ValueError, + match="MultiNorm must be assigned an iterable"): + mcolors.MultiNorm("linear") + with pytest.raises(ValueError, + match="MultiNorm must be assigned at least one"): + mcolors.MultiNorm([]) + with pytest.raises(ValueError, + match="MultiNorm must be assigned an iterable"): + mcolors.MultiNorm(None) + with pytest.raises(ValueError, + match="not a valid"): + mcolors.MultiNorm(["linear", "bad_norm_name"]) + with pytest.raises(ValueError, + match="Each norm assigned to MultiNorm"): + mcolors.MultiNorm(["linear", object()]) + + norm = mpl.colors.MultiNorm(['linear', 'linear']) + + +def test_multi_norm_call_vmin_vmax(): + # test get vmin, vmax + norm = mpl.colors.MultiNorm(['linear', 'log']) + norm.vmin = (1, 1) + norm.vmax = (2, 2) + assert norm.vmin == (1, 1) + assert norm.vmax == (2, 2) + + with pytest.raises(ValueError, match="Expected an iterable of length 2"): + norm.vmin = 1 + with pytest.raises(ValueError, match="Expected an iterable of length 2"): + norm.vmax = 1 + with pytest.raises(ValueError, match="Expected an iterable of length 2"): + norm.vmin = (1, 2, 3) + with pytest.raises(ValueError, match="Expected an iterable of length 2"): + norm.vmax = (1, 2, 3) + + +def test_multi_norm_call_clip_inverse(): + # test get vmin, vmax + norm = mpl.colors.MultiNorm(['linear', 'log']) + norm.vmin = (1, 1) + norm.vmax = (2, 2) + + # test call with clip + assert_array_equal(norm([3, 3], clip=[False, False]), [2.0, 1.584962500721156]) + assert_array_equal(norm([3, 3], clip=[True, True]), [1.0, 1.0]) + assert_array_equal(norm([3, 3], clip=[True, False]), [1.0, 1.584962500721156]) + norm.clip = [False, False] + assert_array_equal(norm([3, 3]), [2.0, 1.584962500721156]) + norm.clip = [True, True] + assert_array_equal(norm([3, 3]), [1.0, 1.0]) + norm.clip = [True, False] + assert_array_equal(norm([3, 3]), [1.0, 1.584962500721156]) + norm.clip = [True, True] + + with pytest.raises(ValueError, match="Expected an iterable of length 2"): + norm.clip = True + with pytest.raises(ValueError, match="Expected an iterable of length 2"): + norm.clip = [True, False, True] + with pytest.raises(ValueError, match="Expected an iterable of length 2"): + norm([3, 3], clip=True) + with pytest.raises(ValueError, match="Expected an iterable of length 2"): + norm([3, 3], clip=[True, True, True]) + + # test inverse + assert_array_almost_equal(norm.inverse([0.5, 0.5849625007211562]), [1.5, 1.5]) + + +def test_multi_norm_autoscale(): + norm = mpl.colors.MultiNorm(['linear', 'log']) + # test autoscale + norm.autoscale([[0, 1, 2, 3], [0.1, 1, 2, 3]]) + assert_array_equal(norm.vmin, [0, 0.1]) + assert_array_equal(norm.vmax, [3, 3]) + + # test autoscale_none + norm0 = mcolors.TwoSlopeNorm(2, vmin=0, vmax=None) + norm = mcolors.MultiNorm([norm0, 'linear'], vmax=[None, 50]) + norm.autoscale_None([[1, 2, 3, 4, 5], [-50, 1, 0, 1, 500]]) + assert_array_equal(norm([5, 0]), [1, 0.5]) + assert_array_equal(norm.vmin, (0, -50)) + assert_array_equal(norm.vmax, (5, 50)) + + +def test_mult_norm_call_types(): + mn = mpl.colors.MultiNorm(['linear', 'linear']) + mn.vmin = (-2, -2) + mn.vmax = (2, 2) + + vals = np.arange(6).reshape((3,2)) + target = np.ma.array([(0.5, 0.75), + (1., 1.25), + (1.5, 1.75)]) + + # test structured array as input + from_mn = mn(rfn.unstructured_to_structured(vals)) + assert_array_almost_equal(from_mn, + target.T) + + # test list of arrays as input + assert_array_almost_equal(mn(list(vals.T)), + list(target.T)) + # test list of floats as input + assert_array_almost_equal(mn(list(vals[0])), + list(target[0])) + # test tuple of arrays as input + assert_array_almost_equal(mn(tuple(vals.T)), + list(target.T)) + + # np.arrays of shapes that are compatible + assert_array_almost_equal(mn(np.zeros(2)), + 0.5*np.ones(2)) + assert_array_almost_equal(mn(np.zeros((2, 3))), + 0.5*np.ones((2, 3))) + assert_array_almost_equal(mn(np.zeros((2, 3, 4))), + 0.5*np.ones((2, 3, 4))) + + # test with NoNorm, list as input + mn_no_norm = mpl.colors.MultiNorm(['linear', mcolors.NoNorm()]) + no_norm_out = mn_no_norm(list(vals.T)) + assert_array_almost_equal(no_norm_out, + [[0., 0.5, 1.], + [1, 3, 5]]) + assert no_norm_out[0].dtype == np.dtype('float64') + assert no_norm_out[1].dtype == vals.dtype + + # test with NoNorm, structured array as input + mn_no_norm = mpl.colors.MultiNorm(['linear', mcolors.NoNorm()]) + no_norm_out = mn_no_norm(rfn.unstructured_to_structured(vals)) + assert_array_almost_equal(no_norm_out, + [[0., 0.5, 1.], + [1, 3, 5]]) + + # test single int as input + with pytest.raises(ValueError, + match="component as input, but got 1 instead"): + mn(1) + + # test list of incompatible size + with pytest.raises(ValueError, + match="but got a sequence with 3 elements"): + mn([3, 2, 1]) + + # last axis matches, len(data.shape) > 2 + with pytest.raises(ValueError, + match=(r"`data_as_list = \[data\[..., i\] for i in " + r"range\(data.shape\[-1\]\)\]`")): + mn(np.zeros((3, 3, 2))) + + # last axis matches, len(data.shape) == 2 + with pytest.raises(ValueError, + match=r"You can use `data_transposed = data.T` to convert"): + mn(np.zeros((3, 2))) + + # incompatible arrays where no relevant axis matches + for data in [np.zeros(3), np.zeros((3, 2, 3))]: + with pytest.raises(ValueError, + match=r"but got a sequence with 3 elements"): + mn(data) + + # test incompatible class + with pytest.raises(ValueError, + match="but got 5, data) + mdata = mcolorizer._ensure_multivariate_data(data, 2) + assert np.all(mdata["f0"].mask[:2] == 0) + assert np.all(mdata["f0"].mask[2:] == 1) + assert np.all(mdata["f1"].mask[:2] == 0) + assert np.all(mdata["f1"].mask[2:] == 1) + + # test tuple of data + data = [0, 1] + mdata = mcolorizer._ensure_multivariate_data(data, 2) + assert mdata.shape == () + + # test wrong input size + data = [[0, 1]] + with pytest.raises(ValueError, match="must contain complex numbers"): + mcolorizer._ensure_multivariate_data(data, 2) + data = [[0, 1]] + with pytest.raises(ValueError, match="have a first dimension 3"): + mcolorizer._ensure_multivariate_data(data, 3) + + # test input of ints as list of lists + data = [[0, 0, 0], [1, 1, 1]] + mdata = mcolorizer._ensure_multivariate_data(data, 2) + assert mdata.shape == (3,) + assert mdata.dtype.fields['f0'][0] == np.int_ + assert mdata.dtype.fields['f1'][0] == np.int_ + + # test input of floats, ints as tuple of lists + data = ([0.0, 0.0], [1, 1]) + mdata = mcolorizer._ensure_multivariate_data(data, 2) + assert mdata.shape == (2,) + assert mdata.dtype.fields['f0'][0] == np.float64 + assert mdata.dtype.fields['f1'][0] == np.int_ + + # test input of array of floats + data = np.array([[0.0, 0, 0], [1, 1, 1]]) + mdata = mcolorizer._ensure_multivariate_data(data, 2) + assert mdata.shape == (3,) + assert mdata.dtype.fields['f0'][0] == np.float64 + assert mdata.dtype.fields['f1'][0] == np.float64 + + # test more input dims + data = np.zeros((3, 4, 5, 6)) + mdata = mcolorizer._ensure_multivariate_data(data, 3) + assert mdata.shape == (4, 5, 6) + + +def test_colorizer_multinorm_implicit(): + ca = mcolorizer.Colorizer('BiOrangeBlue') + ca.vmin = (0, 0) + ca.vmax = (1, 1) + + # test call with two single values + data = [0.1, 0.2] + res = (0.098039, 0.149020, 0.2, 1.0) + assert_array_almost_equal(ca.to_rgba(data), res) + + # test call with two 1d arrays + data = [[0.1, 0.2], [0.3, 0.4]] + res = [[0.09803922, 0.19803922, 0.29803922, 1.], + [0.2, 0.3, 0.4, 1.]] + assert_array_almost_equal(ca.to_rgba(data), res) + + # test call with two 2d arrays + data = [np.linspace(0, 1, 12).reshape(3, 4), + np.linspace(1, 0, 12).reshape(3, 4)] + res = np.array([[[0., 0.5, 1., 1.], + [0.09019608, 0.5, 0.90980392, 1.], + [0.18039216, 0.5, 0.81960784, 1.], + [0.27058824, 0.5, 0.72941176, 1.]], + [[0.36470588, 0.5, 0.63529412, 1.], + [0.45490196, 0.5, 0.54509804, 1.], + [0.54509804, 0.5, 0.45490196, 1.], + [0.63529412, 0.5, 0.36470588, 1.]], + [[0.72941176, 0.5, 0.27058824, 1.], + [0.81960784, 0.5, 0.18039216, 1.], + [0.90980392, 0.5, 0.09019608, 1.], + [1., 0.5, 0., 1.]]]) + assert_array_almost_equal(ca.to_rgba(data), res) + + with pytest.raises(ValueError, match=("This MultiNorm has 2 components, " + "but got a sequence with 3 elements")): + ca.to_rgba([0.1, 0.2, 0.3]) + with pytest.raises(ValueError, match=("This MultiNorm has 2 components, " + "but got a sequence with 1 elements")): + ca.to_rgba([[0.1]]) + + # test multivariate + ca = mcolorizer.Colorizer('3VarAddA') + ca.vmin = (-0.1, -0.2, -0.3) + ca.vmax = (0.1, 0.2, 0.3) + + data = [0.1, 0.1, 0.1] + res = (0.712612, 0.896847, 0.954494, 1.0) + assert_array_almost_equal(ca.to_rgba(data), res) + + +def test_colorizer_multinorm_explicit(): + + with pytest.raises(ValueError, match="MultiNorm must be assigned"): + ca = mcolorizer.Colorizer('BiOrangeBlue', 'linear') + + with pytest.raises(TypeError, + match=("'norm' must be an instance of matplotlib.colors.Norm" + ", str or None, not a list")): + ca = mcolorizer.Colorizer('viridis', ['linear', 'linear']) + + with pytest.raises(ValueError, + match=("Invalid norm for multivariate colormap with 2 inputs")): + ca = mcolorizer.Colorizer('BiOrangeBlue', ['linear', 'linear', 'log']) + + # valid explicit construction + ca = mcolorizer.Colorizer('BiOrangeBlue', [mcolors.Normalize(), 'log']) + ca.vmin = (0, 0.01) + ca.vmax = (1, 1) + + # test call with two single values + data = [0.1, 0.2] + res = (0.098039, 0.374510, 0.65098, 1.) + assert_array_almost_equal(ca.to_rgba(data), res) + + +def test_invalid_cmap_n_components_zero(): + class CustomColormap(mcolors.Colormap): + def __init__(self): + super().__init__("custom") + self.n_variates = 0 + + with pytest.raises(ValueError, match='`n_variates` >= 1'): + ca = mcolorizer.Colorizer(CustomColormap()) + + +def test_colorizer_bivar_cmap(): + ca = mcolorizer.Colorizer('BiOrangeBlue', [mcolors.Normalize(), 'log']) + + with pytest.raises(ValueError, match='The colormap viridis'): + ca.cmap = 'viridis' + + cartist = mcolorizer.ColorizingArtist(ca) + cartist.set_array(np.zeros((2, 4, 4))) + + with pytest.raises(ValueError, match='Invalid data entry for multivariate'): + cartist.set_array(np.zeros((3, 4, 4))) + + dt = np.dtype([('x', 'f4'), ('', 'object')]) + with pytest.raises(TypeError, match='converted to a sequence of floats'): + cartist.set_array(np.zeros((2, 4, 4), dtype=dt)) + + with pytest.raises(ValueError, match='all variates must have same shape'): + cartist.set_array((np.zeros(3), np.zeros(4))) + + # ensure masked value is propagated from input + a = np.arange(3) + cartist.set_array((a, np.ma.masked_where(a > 1, a))) + assert np.all(cartist.get_array()['f0'].mask == np.array([0, 0, 0], dtype=bool)) + assert np.all(cartist.get_array()['f1'].mask == np.array([0, 0, 1], dtype=bool)) + + # test clearing data + cartist.set_array(None) + cartist.get_array() is None + + +def test_colorizer_multivar_cmap(): + ca = mcolorizer.Colorizer('3VarAddA', [mcolors.Normalize(), + mcolors.Normalize(), + 'log']) + cartist = mcolorizer.ColorizingArtist(ca) + cartist.set_array(np.zeros((3, 5, 5))) + with pytest.raises(ValueError, match='Complex numbers are incompatible with'): + cartist.set_array(np.zeros((5, 5), dtype='complex128')) diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index a2fa5efe780f..91aaa2fd9172 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -688,6 +688,77 @@ def test_compressed_suptitle(): assert title.get_position()[1] == 0.98 +@image_comparison(['test_compressed_suptitle_colorbar.png'], style='mpl20') +def test_compressed_suptitle_colorbar(): + """Test that colorbars align with axes in compressed layout with suptitle.""" + arr = np.arange(100).reshape((10, 10)) + fig, axs = plt.subplots(ncols=2, figsize=(4, 2), layout='compressed') + + im0 = axs[0].imshow(arr) + im1 = axs[1].imshow(arr) + + cb0 = plt.colorbar(im0, ax=axs[0]) + cb1 = plt.colorbar(im1, ax=axs[1]) + + fig.suptitle('Title') + + # Verify colorbar heights match axes heights + # After layout, colorbar should have same height as parent axes + fig.canvas.draw() + + for ax, cb in zip(axs, [cb0, cb1]): + ax_pos = ax.get_position() + cb_pos = cb.ax.get_position() + + # Check that colorbar height matches axes height (within tolerance) + # Note: We check the actual rendered positions, not the bbox + assert abs(cb_pos.height - ax_pos.height) < 0.01, \ + f"Colorbar height {cb_pos.height} doesn't match axes height {ax_pos.height}" + + # Also verify vertical alignment (y0 and y1 should match) + assert abs(cb_pos.y0 - ax_pos.y0) < 0.01, \ + f"Colorbar y0 {cb_pos.y0} doesn't match axes y0 {ax_pos.y0}" + assert abs(cb_pos.y1 - ax_pos.y1) < 0.01, \ + f"Colorbar y1 {cb_pos.y1} doesn't match axes y1 {ax_pos.y1}" + + +@image_comparison(['test_compressed_supylabel_colorbar.png'], style='mpl20') +def test_compressed_supylabel_colorbar(): + """ + Test that horizontal colorbars align with axes + in compressed layout with supylabel. + """ + arr = np.arange(100).reshape((10, 10)) + fig, axs = plt.subplots(nrows=2, figsize=(3, 4), layout='compressed') + + im0 = axs[0].imshow(arr) + im1 = axs[1].imshow(arr) + + cb0 = plt.colorbar(im0, ax=axs[0], orientation='horizontal') + cb1 = plt.colorbar(im1, ax=axs[1], orientation='horizontal') + + fig.supylabel('Title') + + # Verify colorbar widths match axes widths + # After layout, colorbar should have same width as parent axes + fig.canvas.draw() + + for ax, cb in zip(axs, [cb0, cb1]): + ax_pos = ax.get_position() + cb_pos = cb.ax.get_position() + + # Check that colorbar width matches axes width (within tolerance) + # Note: We check the actual rendered positions, not the bbox + assert abs(cb_pos.width - ax_pos.width) < 0.01, \ + f"Colorbar width {cb_pos.width} doesn't match axes width {ax_pos.width}" + + # Also verify horizontal alignment (x0 and x1 should match) + assert abs(cb_pos.x0 - ax_pos.x0) < 0.01, \ + f"Colorbar x0 {cb_pos.x0} doesn't match axes x0 {ax_pos.x0}" + assert abs(cb_pos.x1 - ax_pos.x1) < 0.01, \ + f"Colorbar x1 {cb_pos.x1} doesn't match axes x1 {ax_pos.x1}" + + @pytest.mark.parametrize('arg, state', [ (True, True), (False, False), diff --git a/lib/matplotlib/tests/test_container.py b/lib/matplotlib/tests/test_container.py index 1e4577c518ae..b7dfe1196685 100644 --- a/lib/matplotlib/tests/test_container.py +++ b/lib/matplotlib/tests/test_container.py @@ -1,4 +1,5 @@ import numpy as np +from numpy.testing import assert_array_equal import matplotlib.pyplot as plt @@ -35,3 +36,43 @@ def test_nonstring_label(): # Test for #26824 plt.bar(np.arange(10), np.random.rand(10), label=1) plt.legend() + + +def test_barcontainer_position_centers__bottoms__tops(): + fig, ax = plt.subplots() + pos = [1, 2, 4] + bottoms = np.array([1, 5, 3]) + heights = np.array([2, 3, 4]) + + container = ax.bar(pos, heights, bottom=bottoms) + assert_array_equal(container.position_centers, pos) + assert_array_equal(container.bottoms, bottoms) + assert_array_equal(container.tops, bottoms + heights) + + container = ax.barh(pos, heights, left=bottoms) + assert_array_equal(container.position_centers, pos) + assert_array_equal(container.bottoms, bottoms) + assert_array_equal(container.tops, bottoms + heights) + + +def test_piecontainer_remove(): + fig, ax = plt.subplots() + pie = ax.pie([2, 3], labels=['foo', 'bar'], autopct="%1.0f%%") + ax.pie_label(pie, ['baz', 'qux']) + assert len(ax.patches) == 2 + assert len(ax.texts) == 6 + + pie.remove() + assert not ax.patches + assert not ax.texts + + +def test_piecontainer_unpack_backcompat(): + fig, ax = plt.subplots() + wedges, texts, autotexts = ax.pie( + [2, 3], labels=['foo', 'bar'], autopct="%1.0f%%", labeldistance=None) + + assert len(wedges) == 2 + assert isinstance(texts, list) + assert not texts + assert len(autotexts) == 2 diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index c001fc0a9191..f397aee2c9c4 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -127,8 +127,9 @@ def test_contour_manual_moveto(): assert clabels[0].get_text() == "0" +# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(['contour_disconnected_segments'], - remove_text=True, style='mpl20', extensions=['png']) + remove_text=True, style='mpl20', extensions=['png'], tol=0.01) def test_contour_label_with_disconnected_segments(): x, y = np.mgrid[-1:1:21j, -1:1:21j] z = 1 / np.sqrt(0.01 + (x + 0.3) ** 2 + y ** 2) @@ -229,7 +230,8 @@ def test_lognorm_levels(n_levels): assert len(visible_levels) <= n_levels + 1 -@image_comparison(['contour_datetime_axis.png'], style='mpl20') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['contour_datetime_axis.png'], style='mpl20', tol=0.3) def test_contour_datetime_axis(): fig = plt.figure() fig.subplots_adjust(hspace=0.4, top=0.98, bottom=.15) @@ -616,8 +618,7 @@ def test_contourf_legend_elements(): cs = plt.contourf(h, levels=[10, 30, 50], colors=['#FFFF00', '#FF00FF', '#00FFFF'], extend='both') - cs.cmap.set_over('red') - cs.cmap.set_under('blue') + cs.cmap = cs.cmap.with_extremes(over='red', under='blue') cs.changed() artists, labels = cs.legend_elements() assert labels == ['$x \\leq -1e+250s$', @@ -865,3 +866,15 @@ def test_contourf_rasterize(): circle = mpatches.Circle([0.5, 0.5], 0.5, transform=ax.transAxes) cs = ax.contourf(data, clip_path=circle, rasterized=True) assert cs._rasterized + + +@check_figures_equal(extensions=["png"]) +def test_contour_aliases(fig_test, fig_ref): + data = np.arange(100).reshape((10, 10)) ** 2 + fig_test.add_subplot().contour(data, linestyle=":") + fig_ref.add_subplot().contour(data, linestyles="dotted") + + +def test_contour_singular_color(): + with pytest.raises(TypeError): + plt.figure().add_subplot().contour([[0, 1], [2, 3]], color="r") diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index 8ee12131fdbe..d3f64d73002e 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -152,7 +152,8 @@ def test_date_axhspan(): fig.subplots_adjust(left=0.25) -@image_comparison(['date_axvspan.png']) +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['date_axvspan.png'], tol=0.07) def test_date_axvspan(): # test axvspan with date inputs t0 = datetime.datetime(2000, 1, 20) @@ -176,7 +177,8 @@ def test_date_axhline(): fig.subplots_adjust(left=0.25) -@image_comparison(['date_axvline.png']) +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['date_axvline.png'], tol=0.09) def test_date_axvline(): # test axvline with date inputs t0 = datetime.datetime(2000, 1, 20) @@ -226,7 +228,8 @@ def wrapper(): return wrapper -@image_comparison(['RRuleLocator_bounds.png']) +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['RRuleLocator_bounds.png'], tol=0.07) def test_RRuleLocator(): import matplotlib.testing.jpl_units as units units.register() @@ -270,12 +273,13 @@ def test_RRuleLocator_close_minmax(): assert list(map(str, mdates.num2date(loc.tick_values(d1, d2)))) == expected -@image_comparison(['DateFormatter_fractionalSeconds.png']) +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['DateFormatter_fractionalSeconds.png'], tol=0.11) def test_DateFormatter(): import matplotlib.testing.jpl_units as units units.register() - # Lets make sure that DateFormatter will allow us to have tick marks + # Let's make sure that DateFormatter will allow us to have tick marks # at intervals of fractional seconds. t0 = datetime.datetime(2001, 1, 1, 0, 0, 0) @@ -373,7 +377,7 @@ def test_drange(): end = datetime.datetime(2011, 1, 2, tzinfo=mdates.UTC) delta = datetime.timedelta(hours=1) # We expect 24 values in drange(start, end, delta), because drange returns - # dates from an half open interval [start, end) + # dates from a half open interval [start, end) assert len(mdates.drange(start, end, delta)) == 24 # Same if interval ends slightly earlier diff --git a/lib/matplotlib/tests/test_datetime.py b/lib/matplotlib/tests/test_datetime.py index 821552befcf0..b3bd4d7bd151 100644 --- a/lib/matplotlib/tests/test_datetime.py +++ b/lib/matplotlib/tests/test_datetime.py @@ -810,11 +810,32 @@ def test_triplot(self): fig, ax = plt.subplots() ax.triplot(...) - @pytest.mark.xfail(reason="Test for violin not written yet") + @pytest.mark.parametrize("orientation", ["vertical", "horizontal"]) @mpl.style.context("default") - def test_violin(self): + def test_violin(self, orientation): fig, ax = plt.subplots() - ax.violin(...) + datetimes = [ + datetime.datetime(2023, 2, 10), + datetime.datetime(2023, 5, 18), + datetime.datetime(2023, 6, 6) + ] + ax.violin( + [ + { + 'coords': datetimes, + 'vals': [0.1, 0.5, 0.2], + 'mean': datetimes[1], + 'median': datetimes[1], + 'min': datetimes[0], + 'max': datetimes[-1], + 'quantiles': datetimes + } + ], + orientation=orientation, + # TODO: It should be possible for positions to be datetimes too + # https://github.com/matplotlib/matplotlib/issues/30417 + # positions=[datetime.datetime(2020, 1, 1)] + ) @pytest.mark.xfail(reason="Test for violinplot not written yet") @mpl.style.context("default") diff --git a/lib/matplotlib/tests/test_determinism.py b/lib/matplotlib/tests/test_determinism.py index 2ecc40dbd3c0..c0e4adbef40b 100644 --- a/lib/matplotlib/tests/test_determinism.py +++ b/lib/matplotlib/tests/test_determinism.py @@ -26,21 +26,19 @@ def _save_figure(objects='mhip', fmt="pdf", usetex=False): mpl.use(fmt) mpl.rcParams.update({'svg.hashsalt': 'asdf', 'text.usetex': usetex}) - fig = plt.figure() - - if 'm' in objects: + def plot_markers(fig): # use different markers... - ax1 = fig.add_subplot(1, 6, 1) + ax = fig.add_subplot() x = range(10) - ax1.plot(x, [1] * 10, marker='D') - ax1.plot(x, [2] * 10, marker='x') - ax1.plot(x, [3] * 10, marker='^') - ax1.plot(x, [4] * 10, marker='H') - ax1.plot(x, [5] * 10, marker='v') + ax.plot(x, [1] * 10, marker='D') + ax.plot(x, [2] * 10, marker='x') + ax.plot(x, [3] * 10, marker='^') + ax.plot(x, [4] * 10, marker='H') + ax.plot(x, [5] * 10, marker='v') - if 'h' in objects: + def plot_hatch(fig): # also use different hatch patterns - ax2 = fig.add_subplot(1, 6, 2) + ax2 = fig.add_subplot() bars = (ax2.bar(range(1, 5), range(1, 5)) + ax2.bar(range(1, 5), [6] * 4, bottom=range(1, 5))) ax2.set_xticks([1.5, 2.5, 3.5, 4.5]) @@ -49,17 +47,17 @@ def _save_figure(objects='mhip', fmt="pdf", usetex=False): for bar, pattern in zip(bars, patterns): bar.set_hatch(pattern) - if 'i' in objects: + def plot_image(fig): + axs = fig.subplots(1, 3, sharex=True, sharey=True) # also use different images A = [[1, 2, 3], [2, 3, 1], [3, 1, 2]] - fig.add_subplot(1, 6, 3).imshow(A, interpolation='nearest') + axs[0].imshow(A, interpolation='nearest') A = [[1, 3, 2], [1, 2, 3], [3, 1, 2]] - fig.add_subplot(1, 6, 4).imshow(A, interpolation='bilinear') + axs[1].imshow(A, interpolation='bilinear') A = [[2, 3, 1], [1, 2, 3], [2, 1, 3]] - fig.add_subplot(1, 6, 5).imshow(A, interpolation='bicubic') - - if 'p' in objects: + axs[2].imshow(A, interpolation='bicubic') + def plot_paths(fig): # clipping support class, copied from demo_text_path.py gallery example class PathClippedImagePatch(PathPatch): """ @@ -85,13 +83,15 @@ def draw(self, renderer=None): self.bbox_image.draw(renderer) super().draw(renderer) + subfigs = fig.subfigures(1, 3) + # add a polar projection - px = fig.add_subplot(projection="polar") + px = subfigs[0].add_subplot(projection="polar") pimg = px.imshow([[2]]) pimg.set_clip_path(Circle((0, 1), radius=0.3333)) # add a text-based clipping path (origin: demo_text_path.py) - (ax1, ax2) = fig.subplots(2) + ax = subfigs[1].add_subplot() arr = plt.imread(get_sample_data("grace_hopper.jpg")) text_path = TextPath((0, 0), "!?", size=150) p = PathClippedImagePatch(text_path, arr, ec="k") @@ -99,7 +99,7 @@ def draw(self, renderer=None): offsetbox.add_artist(p) ao = AnchoredOffsetbox(loc='upper left', child=offsetbox, frameon=True, borderpad=0.2) - ax1.add_artist(ao) + ax.add_artist(ao) # add a 2x2 grid of path-clipped axes (origin: test_artist.py) exterior = Path.unit_rectangle().deepcopy() @@ -112,7 +112,8 @@ def draw(self, renderer=None): star = Path.unit_regular_star(6).deepcopy() star.vertices *= 2.6 - (row1, row2) = fig.subplots(2, 2, sharex=True, sharey=True) + (row1, row2) = subfigs[2].subplots(2, 2, sharex=True, sharey=True, + gridspec_kw=dict(hspace=0, wspace=0)) for row in (row1, row2): ax1, ax2 = row collection = PathCollection([star], lw=5, edgecolor='blue', @@ -128,8 +129,22 @@ def draw(self, renderer=None): ax1.set_xlim([-3, 3]) ax1.set_ylim([-3, 3]) + nfigs = len(objects) + 1 + fig = plt.figure(figsize=(7, 3 * nfigs)) + subfigs = iter(fig.subfigures(nfigs, squeeze=False).flat) + fig.subplots_adjust(bottom=0.15) + + if 'm' in objects: + plot_markers(next(subfigs)) + if 'h' in objects: + plot_hatch(next(subfigs)) + if 'i' in objects: + plot_image(next(subfigs)) + if 'p' in objects: + plot_paths(next(subfigs)) + x = range(5) - ax = fig.add_subplot(1, 6, 6) + ax = next(subfigs).add_subplot() ax.plot(x, x) ax.set_title('A string $1+2+\\sigma$') ax.set_xlabel('A string $1+2+\\sigma$') @@ -147,8 +162,7 @@ def draw(self, renderer=None): ("i", "pdf", False), ("mhip", "pdf", False), ("mhip", "ps", False), - pytest.param( - "mhip", "ps", True, marks=[needs_usetex, needs_ghostscript]), + pytest.param("mhip", "ps", True, marks=[needs_usetex, needs_ghostscript]), ("p", "svg", False), ("mhip", "svg", False), pytest.param("mhip", "svg", True, marks=needs_usetex), @@ -156,8 +170,7 @@ def draw(self, renderer=None): ) def test_determinism_check(objects, fmt, usetex): """ - Output three times the same graphs and checks that the outputs are exactly - the same. + Output the same graph three times and check that the outputs are exactly the same. Parameters ---------- @@ -197,10 +210,11 @@ def test_determinism_check(objects, fmt, usetex): ) def test_determinism_source_date_epoch(fmt, string): """ - Test SOURCE_DATE_EPOCH support. Output a document with the environment - variable SOURCE_DATE_EPOCH set to 2000-01-01 00:00 UTC and check that the - document contains the timestamp that corresponds to this date (given as an - argument). + Test SOURCE_DATE_EPOCH support. + + Output a document with the environment variable SOURCE_DATE_EPOCH set to + 2000-01-01 00:00 UTC and check that the document contains the timestamp that + corresponds to this date (given as an argument). Parameters ---------- diff --git a/lib/matplotlib/tests/test_doc.py b/lib/matplotlib/tests/test_doc.py index 3e28fd1b8eb7..f3d6d6e3fd5d 100644 --- a/lib/matplotlib/tests/test_doc.py +++ b/lib/matplotlib/tests/test_doc.py @@ -7,9 +7,9 @@ def test_sphinx_gallery_example_header(): This test monitors that the version we have copied is still the same as the EXAMPLE_HEADER in sphinx-gallery. If sphinx-gallery changes its EXAMPLE_HEADER, this test will start to fail. In that case, please update - the monkey-patching of EXAMPLE_HEADER in conf.py. + the monkey-patching of EXAMPLE_HEADER in sphinxext/util.py. """ - pytest.importorskip('sphinx_gallery', minversion='0.16.0') + pytest.importorskip('sphinx_gallery', minversion='0.20.0') from sphinx_gallery import gen_rst EXAMPLE_HEADER = """ @@ -25,7 +25,7 @@ def test_sphinx_gallery_example_header(): :class: sphx-glr-download-link-note :ref:`Go to the end ` - to download the full example code.{2} + to download the full example code{2} .. rst-class:: sphx-glr-example-title diff --git a/lib/matplotlib/tests/test_dviread.py b/lib/matplotlib/tests/test_dviread.py index 7b7ff151be18..33fe9bb150d2 100644 --- a/lib/matplotlib/tests/test_dviread.py +++ b/lib/matplotlib/tests/test_dviread.py @@ -2,7 +2,8 @@ from pathlib import Path import shutil -import matplotlib.dviread as dr +from matplotlib import cbook, dviread as dr +from matplotlib.testing import subprocess_run_for_testing, _has_tex_package import pytest @@ -62,16 +63,85 @@ def test_PsfontsMap(monkeypatch): @pytest.mark.skipif(shutil.which("kpsewhich") is None, reason="kpsewhich is not available") -def test_dviread(): - dirpath = Path(__file__).parent / 'baseline_images/dviread' - with (dirpath / 'test.json').open() as f: - correct = json.load(f) - with dr.Dvi(str(dirpath / 'test.dvi'), None) as dvi: - data = [{'text': [[t.x, t.y, - chr(t.glyph), - t.font.texname.decode('ascii'), - round(t.font.size, 2)] - for t in page.text], - 'boxes': [[b.x, b.y, b.height, b.width] for b in page.boxes]} - for page in dvi] +@pytest.mark.parametrize("engine", ["pdflatex", "xelatex", "lualatex"]) +def test_dviread(tmp_path, engine, monkeypatch): + dirpath = Path(__file__).parent / "baseline_images/dviread" + shutil.copy(dirpath / "test.tex", tmp_path) + shutil.copy(cbook._get_data_path("fonts/ttf/DejaVuSans.ttf"), tmp_path) + cmd, fmt = { + "pdflatex": (["latex"], "dvi"), + "xelatex": (["xelatex", "-no-pdf"], "xdv"), + "lualatex": (["lualatex", "-output-format=dvi"], "dvi"), + }[engine] + if shutil.which(cmd[0]) is None: + pytest.skip(f"{cmd[0]} is not available") + subprocess_run_for_testing( + [*cmd, "test.tex"], cwd=tmp_path, check=True, capture_output=True) + # dviread must be run from the tmppath directory because {xe,lua}tex output + # records the path to DejaVuSans.ttf as it is written in the tex source, + # i.e. as a relative path. + monkeypatch.chdir(tmp_path) + with dr.Dvi(tmp_path / f"test.{fmt}", None) as dvi: + try: + pages = [*dvi] + except FileNotFoundError as exc: + for note in getattr(exc, "__notes__", []): + if "too-old version of luaotfload" in note: + pytest.skip(note) + raise + data = [ + { + "text": [ + [ + t.x, t.y, + t._as_unicode_or_name(), + t.font.resolve_path().name, + round(t.font.size, 2), + t.font.effects, + ] for t in page.text + ], + "boxes": [[b.x, b.y, b.height, b.width] for b in page.boxes] + } for page in pages + ] + correct = json.loads((dirpath / f"{engine}.json").read_text()) + assert data == correct + + +@pytest.mark.skipif(shutil.which("latex") is None, reason="latex is not available") +@pytest.mark.skipif(not _has_tex_package("concmath"), reason="needs concmath.sty") +def test_dviread_pk(tmp_path): + (tmp_path / "test.tex").write_text(r""" + \documentclass{article} + \usepackage{concmath} + \pagestyle{empty} + \begin{document} + Hi! + \end{document} + """) + subprocess_run_for_testing( + ["latex", "test.tex"], cwd=tmp_path, check=True, capture_output=True) + with dr.Dvi(tmp_path / "test.dvi", None) as dvi: + pages = [*dvi] + data = [ + { + "text": [ + [ + t.x, t.y, + t._as_unicode_or_name(), + t.font.resolve_path().name, + round(t.font.size, 2), + t.font.effects, + ] for t in page.text + ], + "boxes": [[b.x, b.y, b.height, b.width] for b in page.boxes] + } for page in pages + ] + correct = [{ + 'boxes': [], + 'text': [ + [5046272, 4128768, 'H?', 'ccr10.600pk', 9.96, {}], + [5530510, 4128768, 'i?', 'ccr10.600pk', 9.96, {}], + [5716195, 4128768, '!?', 'ccr10.600pk', 9.96, {}], + ], + }] assert data == correct diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index c5890a2963b3..95568d237b91 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -25,8 +25,9 @@ import matplotlib.dates as mdates +# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(['figure_align_labels'], extensions=['png', 'svg'], - tol=0 if platform.machine() == 'x86_64' else 0.01) + tol=0.1 if platform.machine() == 'x86_64' else 0.1) def test_align_labels(): fig = plt.figure(layout='tight') gs = gridspec.GridSpec(3, 3) @@ -66,9 +67,10 @@ def test_align_labels(): fig.align_labels() +# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(['figure_align_titles_tight.png', 'figure_align_titles_constrained.png'], - tol=0 if platform.machine() == 'x86_64' else 0.022, + tol=0.3 if platform.machine() == 'x86_64' else 0.04, style='mpl20') def test_align_titles(): for layout in ['tight', 'constrained']: @@ -147,8 +149,6 @@ def test_figure_label(): assert plt.get_figlabels() == ['', 'today'] plt.figure(fig_today) assert plt.gcf() == fig_today - with pytest.raises(ValueError): - plt.figure(Figure()) def test_figure_label_replaced(): @@ -322,7 +322,8 @@ def test_add_subplot_invalid(): fig.add_subplot(ax) -@image_comparison(['figure_suptitle.png']) +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['figure_suptitle.png'], tol=0.02) def test_suptitle(): fig, _ = plt.subplots() fig.suptitle('hello', color='r') @@ -1398,8 +1399,9 @@ def test_subfigure_ss(): fig.suptitle('Figure suptitle', fontsize='xx-large') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(['test_subfigure_double.png'], style='mpl20', - savefig_kwarg={'facecolor': 'teal'}) + savefig_kwarg={'facecolor': 'teal'}, tol=0.02) def test_subfigure_double(): # test assigning the subfigure via subplotspec np.random.seed(19680801) @@ -1690,6 +1692,9 @@ def test_unpickle_with_device_pixel_ratio(): assert fig.dpi == 42*7 fig2 = pickle.loads(pickle.dumps(fig)) assert fig2.dpi == 42 + assert all( + [orig / 7 == restore for orig, restore in zip(fig.bbox.max, fig2.bbox.max)] + ) def test_gridspec_no_mutate_input(): diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 97ee8672b1d4..24421b8e30b3 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -15,7 +15,8 @@ from matplotlib.font_manager import ( findfont, findSystemFonts, FontEntry, FontProperties, fontManager, json_dump, json_load, get_font, is_opentype_cff_font, - MSUserFontDirectories, _get_fontconfig_fonts, ttfFontProperty) + MSUserFontDirectories, ttfFontProperty, + _get_fontconfig_fonts, _normalize_weight) from matplotlib import cbook, ft2font, pyplot as plt, rc_context, figure as mfigure from matplotlib.testing import subprocess_run_helper, subprocess_run_for_testing @@ -407,3 +408,29 @@ def test_fontproperties_init_deprecation(): # 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") + + +def test_normalize_weights(): + assert _normalize_weight(300) == 300 # passthrough + assert _normalize_weight('ultralight') == 100 + assert _normalize_weight('light') == 200 + assert _normalize_weight('normal') == 400 + assert _normalize_weight('regular') == 400 + assert _normalize_weight('book') == 400 + assert _normalize_weight('medium') == 500 + assert _normalize_weight('roman') == 500 + assert _normalize_weight('semibold') == 600 + assert _normalize_weight('demibold') == 600 + assert _normalize_weight('demi') == 600 + assert _normalize_weight('bold') == 700 + assert _normalize_weight('heavy') == 800 + assert _normalize_weight('extra bold') == 800 + assert _normalize_weight('black') == 900 + with pytest.raises(KeyError): + _normalize_weight('invalid') + + +def test_font_match_warning(caplog): + findfont(FontProperties(family=["DejaVu Sans"], weight=750)) + logs = [rec.message for rec in caplog.records] + assert 'findfont: Failed to find font weight 750, now using 700.' in logs diff --git a/lib/matplotlib/tests/test_getattr.py b/lib/matplotlib/tests/test_getattr.py index f0f5823600ca..fe302220067a 100644 --- a/lib/matplotlib/tests/test_getattr.py +++ b/lib/matplotlib/tests/test_getattr.py @@ -1,25 +1,29 @@ from importlib import import_module from pkgutil import walk_packages +import sys +import warnings -import matplotlib import pytest +import matplotlib +from matplotlib.testing import is_ci_environment, subprocess_run_helper + # Get the names of all matplotlib submodules, # except for the unit tests and private modules. -module_names = [ - m.name - for m in walk_packages( - path=matplotlib.__path__, prefix=f'{matplotlib.__name__}.' - ) - if not m.name.startswith(__package__) - and not any(x.startswith('_') for x in m.name.split('.')) -] +module_names = [] +backend_module_names = [] +for m in walk_packages(path=matplotlib.__path__, prefix=f'{matplotlib.__name__}.'): + if m.name.startswith(__package__): + continue + if any(x.startswith('_') for x in m.name.split('.')): + continue + if 'backends.backend_' in m.name: + backend_module_names.append(m.name) + else: + module_names.append(m.name) -@pytest.mark.parametrize('module_name', module_names) -@pytest.mark.filterwarnings('ignore::DeprecationWarning') -@pytest.mark.filterwarnings('ignore::ImportWarning') -def test_getattr(module_name): +def _test_getattr(module_name, use_pytest=True): """ Test that __getattr__ methods raise AttributeError for unknown keys. See #20822, #20855. @@ -28,8 +32,35 @@ def test_getattr(module_name): module = import_module(module_name) except (ImportError, RuntimeError, OSError) as e: # Skip modules that cannot be imported due to missing dependencies - pytest.skip(f'Cannot import {module_name} due to {e}') + if use_pytest: + pytest.skip(f'Cannot import {module_name} due to {e}') + else: + print(f'SKIP: Cannot import {module_name} due to {e}') + return key = 'THIS_SYMBOL_SHOULD_NOT_EXIST' if hasattr(module, key): delattr(module, key) + + +@pytest.mark.parametrize('module_name', module_names) +@pytest.mark.filterwarnings('ignore::DeprecationWarning') +@pytest.mark.filterwarnings('ignore::ImportWarning') +def test_getattr(module_name): + _test_getattr(module_name) + + +def _test_module_getattr(): + warnings.filterwarnings('ignore', category=DeprecationWarning) + warnings.filterwarnings('ignore', category=ImportWarning) + module_name = sys.argv[1] + _test_getattr(module_name, use_pytest=False) + + +@pytest.mark.parametrize('module_name', backend_module_names) +def test_backend_getattr(module_name): + proc = subprocess_run_helper(_test_module_getattr, module_name, + timeout=120 if is_ci_environment() else 20) + if 'SKIP: ' in proc.stdout: + pytest.skip(proc.stdout.removeprefix('SKIP: ')) + print(proc.stdout) diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index 00c223c59362..da7a198a2a94 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -1,5 +1,4 @@ from contextlib import ExitStack -from copy import copy import functools import io import os @@ -37,7 +36,7 @@ def test_alpha_interp(): axr.imshow(img, interpolation="bilinear") -@image_comparison(['interp_nearest_vs_none'], +@image_comparison(['interp_nearest_vs_none'], tol=3.7, # For Ghostscript 10.06+. extensions=['pdf', 'svg'], remove_text=True) def test_interp_nearest_vs_none(): """Test the effect of "nearest" and "none" interpolation""" @@ -453,6 +452,43 @@ def test_format_cursor_data(data, text): assert im.format_cursor_data(im.get_cursor_data(event)) == text +@pytest.mark.parametrize( + "data, text", [ + ([[[10001, 10000]], [[0, 0]]], "[10001.000, 0.000]"), + ([[[.123, .987]], [[0.1, 0]]], "[0.123, 0.100]"), + ([[[np.nan, 1, 2]], [[0, 0, 0]]], "[]"), + ]) +def test_format_cursor_data_multinorm(data, text): + from matplotlib.backend_bases import MouseEvent + fig, ax = plt.subplots() + cmap_bivar = mpl.bivar_colormaps['BiOrangeBlue'] + cmap_multivar = mpl.multivar_colormaps['2VarAddA'] + + # This is a test for ColorizingArtist._format_cursor_data_override() + # with data with multiple channels. + # It includes a workaround so that we can test this functionality + # before the MultiVar/BiVariate colormaps and MultiNorm are exposed + # via the top-level methods (ax.imshow()) + # i.e. we here set the hidden variables _cmap and _norm + # and use set_array() on the ColorizingArtist rather than the _ImageBase + # but this workaround should be replaced by: + # `ax.imshow(data, cmap=cmap_bivar, vmin=(0,0), vmax=(1,1))` + # once the functionality is available. + # see https://github.com/matplotlib/matplotlib/issues/14168 + im = ax.imshow([[0, 1]]) + im.colorizer._cmap = cmap_bivar + im.colorizer._norm = colors.MultiNorm([im.norm, im.norm]) + mpl.colorizer.ColorizingArtist.set_array(im, data) + + xdisp, ydisp = ax.transData.transform([0, 0]) + event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) + assert im.format_cursor_data(im.get_cursor_data(event)) == text + + im.colorizer._cmap = cmap_multivar + event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) + assert im.format_cursor_data(im.get_cursor_data(event)) == text + + @image_comparison(['image_clip'], style='mpl20') def test_image_clip(): d = [[1, 2], [3, 4]] @@ -1168,21 +1204,6 @@ def test_respects_bbox(): assert buf_before.getvalue() != buf_after.getvalue() # Not all white. -def test_image_cursor_formatting(): - fig, ax = plt.subplots() - # Create a dummy image to be able to call format_cursor_data - im = ax.imshow(np.zeros((4, 4))) - - data = np.ma.masked_array([0], mask=[True]) - assert im.format_cursor_data(data) == '[]' - - data = np.ma.masked_array([0], mask=[False]) - assert im.format_cursor_data(data) == '[0]' - - data = np.nan - assert im.format_cursor_data(data) == '[nan]' - - @check_figures_equal(extensions=['png', 'pdf', 'svg']) def test_image_array_alpha(fig_test, fig_ref): """Per-pixel alpha channel test.""" @@ -1209,8 +1230,7 @@ def test_image_array_alpha_validation(): @mpl.style.context('mpl20') def test_exact_vmin(): - cmap = copy(mpl.colormaps["autumn_r"]) - cmap.set_under(color="lightgrey") + cmap = mpl.colormaps["autumn_r"].with_extremes(under="lightgrey") # make the image exactly 190 pixels wide fig = plt.figure(figsize=(1.9, 0.1), dpi=100) @@ -1484,9 +1504,7 @@ def test_rgba_antialias(): aa[70:90, 195:215] = 1e6 aa[20:30, 195:215] = -1e6 - cmap = plt.colormaps["RdBu_r"] - cmap.set_over('yellow') - cmap.set_under('cyan') + cmap = plt.colormaps["RdBu_r"].with_extremes(over='yellow', under='cyan') axs = axs.flatten() # zoom in @@ -1586,8 +1604,8 @@ def test_large_image(fig_test, fig_ref, dim, size, msg, origin): 'accurately displayed.'): fig_test.canvas.draw() - array = np.zeros((1, 2)) - array[:, 1] = 1 + array = np.zeros((1, size // 2 + 1)) + array[:, array.size // 2:] = 1 if dim == 'col': array = array.T im = ax_ref.imshow(array, vmin=0, vmax=1, aspect='auto', @@ -1645,19 +1663,33 @@ def test__resample_valid_output(): [(np.array([[0.1, 0.3, 0.2]]), mimage.NEAREST, np.array([[0.1, 0.1, 0.1, 0.3, 0.3, 0.3, 0.3, 0.2, 0.2, 0.2]])), (np.array([[0.1, 0.3, 0.2]]), mimage.BILINEAR, - np.array([[0.1, 0.1, 0.15078125, 0.21096191, 0.27033691, - 0.28476562, 0.2546875, 0.22460938, 0.20002441, 0.20002441]])), + np.array([[0.1, 0.1, 0.15, 0.21, 0.27, 0.285, 0.255, 0.225, 0.2, 0.2]])), + (np.array([[0.1, 0.9]]), mimage.BILINEAR, + np.array([[0.1, 0.1, 0.1, 0.1, 0.1, 0.14, 0.22, 0.3, 0.38, 0.46, + 0.54, 0.62, 0.7, 0.78, 0.86, 0.9, 0.9, 0.9, 0.9, 0.9]])), + (np.array([[0.1, 0.1]]), mimage.BILINEAR, np.full((1, 10), 0.1)), + # Test at the subpixel level + (np.array([[0.1, 0.9]]), mimage.NEAREST, + np.concatenate([np.full(512, 0.1), np.full(512, 0.9)]).reshape(1, -1)), + (np.array([[0.1, 0.9]]), mimage.BILINEAR, + np.concatenate([np.full(256, 0.1), + np.linspace(0.5, 256, 512).astype(int) / 256 * 0.8 + 0.1, + np.full(256, 0.9)]).reshape(1, -1)), ] ) def test_resample_nonaffine(data, interpolation, expected): - # Test that equivalent affine and nonaffine transforms resample the same + # Test that both affine and nonaffine transforms resample to the correct answer + + # If the array is constant, the tolerance can be tight + # Otherwise, the tolerance is limited by the subpixel approach in the agg backend + atol = 0 if np.all(data == data.ravel()[0]) else 2e-3 # Create a simple affine transform for scaling the input array affine_transform = Affine2D().scale(sx=expected.shape[1] / data.shape[1], sy=1) affine_result = np.empty_like(expected) mimage.resample(data, affine_result, affine_transform, interpolation=interpolation) - assert_allclose(affine_result, expected) + assert_allclose(affine_result, expected, atol=atol) # Create a nonaffine version of the same transform # by compositing with a nonaffine identity transform @@ -1666,13 +1698,13 @@ class NonAffineIdentityTransform(Transform): output_dims = 2 def inverted(self): - return self + return self nonaffine_transform = NonAffineIdentityTransform() + affine_transform nonaffine_result = np.empty_like(expected) mimage.resample(data, nonaffine_result, nonaffine_transform, interpolation=interpolation) - assert_allclose(nonaffine_result, expected, atol=5e-3) + assert_allclose(nonaffine_result, expected, atol=atol) def test_axesimage_get_shape(): @@ -1741,8 +1773,7 @@ def test_downsampling_speckle(): axs = axs.flatten() img = ((np.arange(1024).reshape(-1, 1) * np.ones(720)) // 50).T - cm = plt.get_cmap("viridis") - cm.set_over("m") + cm = plt.get_cmap("viridis").with_extremes(over="m") norm = colors.LogNorm(vmin=3, vmax=11) # old default cannot be tested because it creates over/under speckles diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 9b100037cc41..5f83b25b90a5 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -1068,6 +1068,201 @@ def test_legend_labelcolor_rcparam_markerfacecolor_short(): assert mpl.colors.same_color(text.get_color(), color) +def assert_last_legend_patch_color(histogram, leg, expected_color, + facecolor=False, edgecolor=False): + """ + Check that histogram color, legend handle color, and legend label color all + match the expected input. Provide facecolor and edgecolor flags to clarify + which feature to match. + """ + label_color = leg.texts[-1].get_color() + patch = leg.get_patches()[-1] + histogram = histogram[-1][0] + assert mpl.colors.same_color(label_color, expected_color) + if facecolor: + assert mpl.colors.same_color(label_color, patch.get_facecolor()) + assert mpl.colors.same_color(label_color, histogram.get_facecolor()) + if edgecolor: + assert mpl.colors.same_color(label_color, patch.get_edgecolor()) + assert mpl.colors.same_color(label_color, histogram.get_edgecolor()) + + +def test_legend_labelcolor_linecolor_histograms(): + x = np.arange(10) + + # testing c kwarg for bar, step, and stepfilled histograms + fig, ax = plt.subplots() + h = ax.hist(x, histtype='bar', color='r', label="red bar hist with a red label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_patch_color(h, leg, 'r', facecolor=True) + + h = ax.hist(x, histtype='step', color='g', label="green step hist, green label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_patch_color(h, leg, 'g', edgecolor=True) + + h = ax.hist(x, histtype='stepfilled', color='b', + label="blue stepfilled hist with a blue label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_patch_color(h, leg, 'b', facecolor=True) + + # testing c, fc, and ec combinations for bar histograms + h = ax.hist(x, histtype='bar', color='r', ec='b', + label="red bar hist with blue edges and a red label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_patch_color(h, leg, 'r', facecolor=True) + + h = ax.hist(x, histtype='bar', fc='r', ec='b', + label="red bar hist with blue edges and a red label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_patch_color(h, leg, 'r', facecolor=True) + + h = ax.hist(x, histtype='bar', fc='none', ec='b', + label="unfilled blue bar hist with a blue label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_patch_color(h, leg, 'b', edgecolor=True) + + # testing c, and ec combinations for step histograms + h = ax.hist(x, histtype='step', color='r', ec='b', + label="blue step hist with a blue label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_patch_color(h, leg, 'b', edgecolor=True) + + h = ax.hist(x, histtype='step', ec='b', + label="blue step hist with a blue label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_patch_color(h, leg, 'b', edgecolor=True) + + # testing c, fc, and ec combinations for stepfilled histograms + h = ax.hist(x, histtype='stepfilled', color='r', ec='b', + label="red stepfilled hist, blue edges, red label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_patch_color(h, leg, 'r', facecolor=True) + + h = ax.hist(x, histtype='stepfilled', fc='r', ec='b', + label="red stepfilled hist, blue edges, red label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_patch_color(h, leg, 'r', facecolor=True) + + h = ax.hist(x, histtype='stepfilled', fc='none', ec='b', + label="unfilled blue stepfilled hist, blue label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_patch_color(h, leg, 'b', edgecolor=True) + + h = ax.hist(x, histtype='stepfilled', fc='r', ec='none', + label="edgeless red stepfilled hist with a red label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_patch_color(h, leg, 'r', facecolor=True) + + +def assert_last_legend_linemarker_color(line_marker, leg, expected_color, color=False, + facecolor=False, edgecolor=False): + """ + Check that line marker color, legend handle color, and legend label color all + match the expected input. Provide color, facecolor and edgecolor flags to clarify + which feature to match. + """ + label_color = leg.texts[-1].get_color() + leg_marker = leg.get_lines()[-1] + assert mpl.colors.same_color(label_color, expected_color) + if color: + assert mpl.colors.same_color(label_color, leg_marker.get_color()) + assert mpl.colors.same_color(label_color, line_marker.get_color()) + if facecolor: + assert mpl.colors.same_color(label_color, leg_marker.get_markerfacecolor()) + assert mpl.colors.same_color(label_color, line_marker.get_markerfacecolor()) + if edgecolor: + assert mpl.colors.same_color(label_color, leg_marker.get_markeredgecolor()) + assert mpl.colors.same_color(label_color, line_marker.get_markeredgecolor()) + + +def test_legend_labelcolor_linecolor_plot(): + x = np.arange(5) + + # testing line plot + fig, ax = plt.subplots() + l, = ax.plot(x, c='r', label="red line with a red label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_linemarker_color(l, leg, 'r', color=True) + + # testing c, fc, and ec combinations for maker plots + l, = ax.plot(x, 'o', c='r', label="red circles with a red label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_linemarker_color(l, leg, 'r', color=True) + + l, = ax.plot(x, 'o', c='r', mec='b', label="red circles, blue edges, red label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_linemarker_color(l, leg, 'r', color=True) + + l, = ax.plot(x, 'o', mfc='r', mec='b', label="red circles, blue edges, red label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_linemarker_color(l, leg, 'r', facecolor=True) + + # 'none' cases + l, = ax.plot(x, 'o', mfc='none', mec='b', + label="blue unfilled circles, blue label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_linemarker_color(l, leg, 'b', edgecolor=True) + + l, = ax.plot(x, 'o', mfc='r', mec='none', label="red edgeless circles, red label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_linemarker_color(l, leg, 'r', facecolor=True) + + l, = ax.plot(x, 'o', c='none', mec='none', + label="black label despite invisible circles for dummy entries") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_linemarker_color(l, leg, 'k') + + +def assert_last_legend_scattermarker_color(scatter_marker, leg, expected_color, + facecolor=False, edgecolor=False): + """ + Check that scatter marker color, legend handle color, and legend label color all + match the expected input. Provide facecolor and edgecolor flags to clarify + which feature to match. + """ + label_color = leg.texts[-1].get_color() + leg_handle = leg.legend_handles[-1] + assert mpl.colors.same_color(label_color, expected_color) + if facecolor: + assert mpl.colors.same_color(label_color, leg_handle.get_facecolor()) + assert mpl.colors.same_color(label_color, scatter_marker.get_facecolor()) + if edgecolor: + assert mpl.colors.same_color(label_color, leg_handle.get_edgecolor()) + assert mpl.colors.same_color(label_color, scatter_marker.get_edgecolor()) + + +def test_legend_labelcolor_linecolor_scatter(): + x = np.arange(5) + + # testing c, fc, and ec combinations for scatter plots + fig, ax = plt.subplots() + s = ax.scatter(x, x, c='r', label="red circles with a red label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_scattermarker_color(s, leg, 'r', facecolor=True) + + s = ax.scatter(x, x, c='r', ec='b', label="red circles, blue edges, red label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_scattermarker_color(s, leg, 'r', facecolor=True) + + s = ax.scatter(x, x, fc='r', ec='b', label="red circles, blue edges, red label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_scattermarker_color(s, leg, 'r', facecolor=True) + + # 'none' cases + s = ax.scatter(x, x, fc='none', ec='b', label="blue unfilled circles, blue label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_scattermarker_color(s, leg, 'b', edgecolor=True) + + s = ax.scatter(x, x, fc='r', ec='none', label="red edgeless circles, red label") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_scattermarker_color(s, leg, 'r', facecolor=True) + + s = ax.scatter(x, x, c='none', ec='none', + label="black label despite invisible circles for dummy entries") + leg = ax.legend(labelcolor='linecolor') + assert_last_legend_scattermarker_color(s, leg, 'k') + + @pytest.mark.filterwarnings("ignore:No artists with labels found to put in legend") def test_get_set_draggable(): legend = plt.legend() @@ -1472,3 +1667,86 @@ def test_boxplot_legend_labels(): bp4 = axs[3].boxplot(data, label='box A') assert bp4['medians'][0].get_label() == 'box A' assert all(x.get_label().startswith("_") for x in bp4['medians'][1:]) + + +def test_legend_linewidth(): + """Test legend.linewidth parameter and rcParam.""" + fig, ax = plt.subplots() + ax.plot([1, 2, 3], label='data') + + # Test direct parameter + leg = ax.legend(linewidth=2.5) + assert leg.legendPatch.get_linewidth() == 2.5 + + # Test rcParam + with mpl.rc_context({'legend.linewidth': 3.0}): + fig, ax = plt.subplots() + ax.plot([1, 2, 3], label='data') + leg = ax.legend() + assert leg.legendPatch.get_linewidth() == 3.0 + + # Test None default (should inherit from patch.linewidth) + with mpl.rc_context({'legend.linewidth': None, 'patch.linewidth': 1.5}): + fig, ax = plt.subplots() + ax.plot([1, 2, 3], label='data') + leg = ax.legend() + assert leg.legendPatch.get_linewidth() == 1.5 + + # Test that direct parameter overrides rcParam + with mpl.rc_context({'legend.linewidth': 1.0}): + fig, ax = plt.subplots() + ax.plot([1, 2, 3], label='data') + leg = ax.legend(linewidth=4.0) + assert leg.legendPatch.get_linewidth() == 4.0 + + +def test_patchcollection_legend(): + # Test that PatchCollection labels show up in legend and preserve visual + # properties (issue #23998) + fig, ax = plt.subplots() + + pc = mcollections.PatchCollection( + [mpatches.Circle((0, 0), 1), mpatches.Circle((2, 0), 1)], + label="patch collection", + facecolor='red', + edgecolor='blue', + linewidths=3, + linestyle='--', + ) + ax.add_collection(pc) + ax.autoscale_view() + + leg = ax.legend() + + # Check that the legend contains our label + assert len(leg.get_texts()) == 1 + assert leg.get_texts()[0].get_text() == "patch collection" + + # Check that the legend handle exists and has correct visual properties + assert len(leg.legend_handles) == 1 + legend_patch = leg.legend_handles[0] + assert mpl.colors.same_color(legend_patch.get_facecolor(), + pc.get_facecolor()[0]) + assert mpl.colors.same_color(legend_patch.get_edgecolor(), + pc.get_edgecolor()[0]) + assert legend_patch.get_linewidth() == pc.get_linewidths()[0] + assert legend_patch.get_linestyle() == pc.get_linestyles()[0] + + +def test_patchcollection_legend_empty(): + # Test that empty PatchCollection doesn't crash + fig, ax = plt.subplots() + + # Create an empty PatchCollection + pc = mcollections.PatchCollection([], label="empty collection") + ax.add_collection(pc) + + # This should not crash + leg = ax.legend() + + # Check that the label still appears + assert len(leg.get_texts()) == 1 + assert leg.get_texts()[0].get_text() == "empty collection" + + # The legend handle should exist + assert len(leg.legend_handles) == 1 diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index fe92547c5963..8bf6fea2cdf7 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -33,7 +33,7 @@ def test_segment_hits(): # Runtimes on a loaded system are inherently flaky. Not so much that a rerun # won't help, hopefully. -@pytest.mark.flaky(reruns=3) +@pytest.mark.flaky(reruns=5) def test_invisible_Line_rendering(): """ GitHub issue #1256 identified a bug in Line.draw method diff --git a/lib/matplotlib/tests/test_mlab.py b/lib/matplotlib/tests/test_mlab.py index 3b0d2529b5f1..109a6d542450 100644 --- a/lib/matplotlib/tests/test_mlab.py +++ b/lib/matplotlib/tests/test_mlab.py @@ -1,3 +1,5 @@ +import sys + from numpy.testing import (assert_allclose, assert_almost_equal, assert_array_equal, assert_array_almost_equal_nulp) import numpy as np @@ -429,7 +431,16 @@ def test_spectral_helper_psd(self, mode, case): assert spec.shape[0] == freqs.shape[0] assert spec.shape[1] == getattr(self, f"t_{case}").shape[0] - def test_csd(self): + @pytest.mark.parametrize('bitsize', [ + pytest.param(None, id='default'), + pytest.param(32, + marks=pytest.mark.skipif(sys.maxsize <= 2**32, + reason='System is already 32-bit'), + id='32-bit') + ]) + def test_csd(self, bitsize, monkeypatch): + if bitsize is not None: + monkeypatch.setattr(sys, 'maxsize', 2**bitsize) freqs = self.freqs_density spec, fsp = mlab.csd(x=self.y, y=self.y+1, NFFT=self.NFFT_density, diff --git a/lib/matplotlib/tests/test_multivariate_colormaps.py b/lib/matplotlib/tests/test_multivariate_colormaps.py index 81a2e6adeb35..592058212a24 100644 --- a/lib/matplotlib/tests/test_multivariate_colormaps.py +++ b/lib/matplotlib/tests/test_multivariate_colormaps.py @@ -212,9 +212,26 @@ def test_multivar_resample(): def test_bivar_cmap_call_tuple(): cmap = mpl.bivar_colormaps['BiOrangeBlue'] - assert_allclose(cmap((1.0, 1.0)), (1, 1, 1, 1), atol=0.01) - assert_allclose(cmap((0.0, 0.0)), (0, 0, 0, 1), atol=0.1) - assert_allclose(cmap((0.0, 0.0), alpha=0.1), (0, 0, 0, 0.1), atol=0.1) + assert_allclose(cmap((1.0, 1.0)), (1, 1, 1, 1)) + assert_allclose(cmap((0.0, 0.0)), (0, 0, 0, 1)) + assert_allclose(cmap((0.2, 0.8)), (0.2, 0.5, 0.8, 1)) + assert_allclose(cmap((0.0, 0.0), alpha=0.1), (0, 0, 0, 0.1)) + + +def test_bivar_cmap_lut_smooth(): + cmap = mpl.bivar_colormaps['BiOrangeBlue'] + + assert_allclose(cmap.lut[:, 0, 0], np.linspace(0, 1, 256)) + assert_allclose(cmap.lut[:, 255, 0], np.linspace(0, 1, 256)) + assert_allclose(cmap.lut[:, 0, 1], np.linspace(0, 0.5, 256)) + assert_allclose(cmap.lut[:, 153, 1], np.linspace(0.3, 0.8, 256)) + assert_allclose(cmap.lut[:, 255, 1], np.linspace(0.5, 1, 256)) + + assert_allclose(cmap.lut[0, :, 1], np.linspace(0, 0.5, 256)) + assert_allclose(cmap.lut[102, :, 1], np.linspace(0.2, 0.7, 256)) + assert_allclose(cmap.lut[255, :, 1], np.linspace(0.5, 1, 256)) + assert_allclose(cmap.lut[0, :, 2], np.linspace(0, 1, 256)) + assert_allclose(cmap.lut[255, :, 2], np.linspace(0, 1, 256)) def test_bivar_cmap_call(): @@ -312,20 +329,36 @@ def test_bivar_cmap_call(): match="only implemented for use with with floats"): cs = cmap([(0, 5, 9, 0, 0, 9), (0, 0, 0, 5, 11, 11)]) - # test origin - cmap = mpl.bivar_colormaps['BiOrangeBlue'].with_extremes(origin=(0.5, 0.5)) - assert_allclose(cmap[0](0.5), - (0.50244140625, 0.5024222412109375, 0.50244140625, 1)) - assert_allclose(cmap[1](0.5), - (0.50244140625, 0.5024222412109375, 0.50244140625, 1)) - cmap = mpl.bivar_colormaps['BiOrangeBlue'].with_extremes(origin=(1, 1)) - assert_allclose(cmap[0](1.), - (0.99853515625, 0.9985467529296875, 0.99853515625, 1.0)) - assert_allclose(cmap[1](1.), - (0.99853515625, 0.9985467529296875, 0.99853515625, 1.0)) + +def test_bivar_cmap_1d_origin(): + """ + Test getting 1D colormaps with different origins + """ + cmap0 = mpl.bivar_colormaps['BiOrangeBlue'] + assert_allclose(cmap0[0].colors[:, 0], np.linspace(0, 1, 256)) + assert_allclose(cmap0[0].colors[:, 1], np.linspace(0, 0.5, 256)) + assert_allclose(cmap0[0].colors[:, 2], 0) + assert_allclose(cmap0[1].colors[:, 0], 0) + assert_allclose(cmap0[1].colors[:, 1], np.linspace(0, 0.5, 256)) + assert_allclose(cmap0[1].colors[:, 2], np.linspace(0, 1, 256)) + + cmap1 = cmap0.with_extremes(origin=(0, 1)) + assert_allclose(cmap1[0].colors[:, 0], np.linspace(0, 1, 256)) + assert_allclose(cmap1[0].colors[:, 1], np.linspace(0.5, 1, 256)) + assert_allclose(cmap1[0].colors[:, 2], 1) + assert_allclose(cmap1[1].colors, cmap0[1].colors) + + cmap2 = cmap0.with_extremes(origin=(0.2, 0.4)) + assert_allclose(cmap2[0].colors[:, 0], np.linspace(0, 1, 256)) + assert_allclose(cmap2[0].colors[:, 1], np.linspace(0.2, 0.7, 256)) + assert_allclose(cmap2[0].colors[:, 2], 0.4) + assert_allclose(cmap2[1].colors[:, 0], 0.2) + assert_allclose(cmap2[1].colors[:, 1], np.linspace(0.1, 0.6, 256)) + assert_allclose(cmap2[1].colors[:, 2], np.linspace(0, 1, 256)) + with pytest.raises(KeyError, match="only 0 or 1 are valid keys"): - cs = cmap[2] + cs = cmap0[2] def test_bivar_getitem(): @@ -433,22 +466,18 @@ def test_bivar_cmap_from_image(): def test_bivar_resample(): - cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((2, 2)) - assert_allclose(cmap((0.25, 0.25)), (0, 0, 0, 1), atol=1e-2) - - cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((-2, 2)) - assert_allclose(cmap((0.25, 0.25)), (1., 0.5, 0., 1.), atol=1e-2) + cmap = mpl.bivar_colormaps['BiOrangeBlue'] - cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((2, -2)) - assert_allclose(cmap((0.25, 0.25)), (0., 0.5, 1., 1.), atol=1e-2) + assert_allclose(cmap.resampled((2, 2))((0.25, 0.25)), (0, 0, 0, 1)) + assert_allclose(cmap.resampled((-2, 2))((0.25, 0.25)), (1., 0.5, 0., 1.)) + assert_allclose(cmap.resampled((2, -2))((0.25, 0.25)), (0., 0.5, 1., 1.)) + assert_allclose(cmap.resampled((-2, -2))((0.25, 0.25)), (1, 1, 1, 1)) - cmap = mpl.bivar_colormaps['BiOrangeBlue'].resampled((-2, -2)) - assert_allclose(cmap((0.25, 0.25)), (1, 1, 1, 1), atol=1e-2) + assert_allclose(cmap((0.8, 0.4)), (0.8, 0.6, 0.4, 1.)) + assert_allclose(cmap.reversed()((1 - 0.8, 1 - 0.4)), (0.8, 0.6, 0.4, 1.)) - cmap = mpl.bivar_colormaps['BiOrangeBlue'].reversed() - assert_allclose(cmap((0.25, 0.25)), (0.748535, 0.748547, 0.748535, 1.), atol=1e-2) - cmap = mpl.bivar_colormaps['BiOrangeBlue'].transposed() - assert_allclose(cmap((0.25, 0.25)), (0.252441, 0.252422, 0.252441, 1.), atol=1e-2) + assert_allclose(cmap((0.6, 0.2)), (0.6, 0.4, 0.2, 1.)) + assert_allclose(cmap.transposed()((0.2, 0.6)), (0.6, 0.4, 0.2, 1.)) with pytest.raises(ValueError, match="lutshape must be of length"): cmap = cmap.resampled(4) diff --git a/lib/matplotlib/tests/test_offsetbox.py b/lib/matplotlib/tests/test_offsetbox.py index bd353ffc719b..f126b1cbb466 100644 --- a/lib/matplotlib/tests/test_offsetbox.py +++ b/lib/matplotlib/tests/test_offsetbox.py @@ -470,3 +470,40 @@ def test_draggable_in_subfigure(): bbox = ann.get_window_extent() MouseEvent("button_press_event", fig.canvas, bbox.x1+2, bbox.y1+2)._process() assert not ann._draggable.got_artist + + +def test_anchored_offsetbox_tuple_and_float_borderpad(): + """ + Test AnchoredOffsetbox correctly handles both float and tuple for borderpad. + """ + + fig, ax = plt.subplots() + + # Case 1: Establish a baseline with float value + text_float = AnchoredText("float", loc='lower left', borderpad=5) + ax.add_artist(text_float) + + # Case 2: Test that a symmetric tuple gives the exact same result. + text_tuple_equal = AnchoredText("tuple", loc='lower left', borderpad=(5, 5)) + ax.add_artist(text_tuple_equal) + + # Case 3: Test that an asymmetric tuple with different values works as expected. + text_tuple_asym = AnchoredText("tuple_asym", loc='lower left', borderpad=(10, 4)) + ax.add_artist(text_tuple_asym) + + # Draw the canvas to calculate final positions + fig.canvas.draw() + + pos_float = text_float.get_window_extent() + pos_tuple_equal = text_tuple_equal.get_window_extent() + pos_tuple_asym = text_tuple_asym.get_window_extent() + + # Assertion 1: Prove that borderpad=5 is identical to borderpad=(5, 5). + assert pos_tuple_equal.x0 == pos_float.x0 + assert pos_tuple_equal.y0 == pos_float.y0 + + # Assertion 2: Prove that the asymmetric padding moved the box + # further from the origin than the baseline in the x-direction and less far + # in the y-direction. + assert pos_tuple_asym.x0 > pos_float.x0 + assert pos_tuple_asym.y0 < pos_float.y0 diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py index d69a9dad4337..ed608eebb6a7 100644 --- a/lib/matplotlib/tests/test_patches.py +++ b/lib/matplotlib/tests/test_patches.py @@ -941,7 +941,9 @@ def test_arc_in_collection(fig_test, fig_ref): arc2 = Arc([.5, .5], .5, 1, theta1=0, theta2=60, angle=20) col = mcollections.PatchCollection(patches=[arc2], facecolors='none', edgecolors='k') - fig_ref.subplots().add_patch(arc1) + ax_ref = fig_ref.subplots() + ax_ref.add_patch(arc1) + ax_ref.autoscale_view() fig_test.subplots().add_collection(col) diff --git a/lib/matplotlib/tests/test_pickle.py b/lib/matplotlib/tests/test_pickle.py index 82fc60e186c7..1590990cdeb0 100644 --- a/lib/matplotlib/tests/test_pickle.py +++ b/lib/matplotlib/tests/test_pickle.py @@ -150,15 +150,7 @@ def test_pickle_load_from_subprocess(fig_test, fig_ref, tmp_path): proc = subprocess_run_helper( _pickle_load_subprocess, timeout=60, - extra_env={ - "PICKLE_FILE_PATH": str(fp), - "MPLBACKEND": "Agg", - # subprocess_run_helper will set SOURCE_DATE_EPOCH=0, so for a dirty tree, - # the version will have the date 19700101. As we aren't trying to test the - # version compatibility warning, force setuptools-scm to use the same - # version as us. - "SETUPTOOLS_SCM_PRETEND_VERSION_FOR_MATPLOTLIB": mpl.__version__, - }, + extra_env={"PICKLE_FILE_PATH": str(fp), "MPLBACKEND": "Agg"}, ) loaded_fig = pickle.loads(ast.literal_eval(proc.stdout)) diff --git a/lib/matplotlib/tests/test_polar.py b/lib/matplotlib/tests/test_polar.py index 4f9e63380490..4cbb099e3293 100644 --- a/lib/matplotlib/tests/test_polar.py +++ b/lib/matplotlib/tests/test_polar.py @@ -214,7 +214,8 @@ def test_polar_theta_position(): ax.set_theta_direction('clockwise') -@image_comparison(['polar_rlabel_position.png'], style='default') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['polar_rlabel_position.png'], style='default', tol=0.07) def test_polar_rlabel_position(): fig = plt.figure() ax = fig.add_subplot(projection='polar') @@ -229,7 +230,8 @@ def test_polar_title_position(): ax.set_title('foo') -@image_comparison(['polar_theta_wedge.png'], style='default') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['polar_theta_wedge.png'], style='default', tol=0.2) def test_polar_theta_limits(): r = np.arange(0, 3.0, 0.01) theta = 2*np.pi*r diff --git a/lib/matplotlib/tests/test_pyplot.py b/lib/matplotlib/tests/test_pyplot.py index 55f7c33cb52e..1cca7332aa0c 100644 --- a/lib/matplotlib/tests/test_pyplot.py +++ b/lib/matplotlib/tests/test_pyplot.py @@ -13,7 +13,7 @@ def test_pyplot_up_to_date(tmp_path): - pytest.importorskip("black") + pytest.importorskip("black", minversion="24.1") gen_script = Path(mpl.__file__).parents[2] / "tools/boilerplate.py" if not gen_script.exists(): @@ -471,6 +471,30 @@ def test_multiple_same_figure_calls(): assert fig is fig3 +def test_register_existing_figure_with_pyplot(): + from matplotlib.figure import Figure + # start with a standalone figure + fig = Figure() + assert fig.canvas.manager is None + with pytest.raises(AttributeError): + # Heads-up: This will change to returning None in the future + # See docstring for the Figure.number property + fig.number + # register the Figure with pyplot + plt.figure(fig) + assert fig.number == 1 + # the figure can now be used in pyplot + plt.suptitle("my title") + assert fig.get_suptitle() == "my title" + # it also has a manager that is properly wired up in the pyplot state + assert plt._pylab_helpers.Gcf.get_fig_manager(fig.number) is fig.canvas.manager + # and we can regularly switch the pyplot state + fig2 = plt.figure() + assert fig2.number == 2 + assert plt.figure(1) is fig + assert plt.gcf() is fig + + def test_close_all_warning(): fig1 = plt.figure() diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 2235f98b720f..eb9d3bc8866b 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -13,6 +13,7 @@ import matplotlib.pyplot as plt import matplotlib.colors as mcolors import numpy as np +from matplotlib import rcsetup from matplotlib.rcsetup import ( validate_bool, validate_color, @@ -672,3 +673,21 @@ def test_rc_aliases(group, option, alias, value): rcParams_key = f"{group}.{option}" assert mpl.rcParams[rcParams_key] == value + + +def test_all_params_defined_as_code(): + assert set(p.name for p in rcsetup._params) == set(mpl.rcParams.keys()) + + +def test_validators_defined_as_code(): + for param in rcsetup._params: + validator = rcsetup._convert_validator_spec(param.name, param.validator) + assert validator == rcsetup._validators[param.name] + + +def test_defaults_as_code(): + for param in rcsetup._params: + if param.name == 'backend': + # backend has special handling and no meaningful default + continue + assert param.default == mpl.rcParamsDefault[param.name], param.name diff --git a/lib/matplotlib/tests/test_sankey.py b/lib/matplotlib/tests/test_sankey.py index 253bfa4fa093..745db5f767b2 100644 --- a/lib/matplotlib/tests/test_sankey.py +++ b/lib/matplotlib/tests/test_sankey.py @@ -6,7 +6,7 @@ def test_sankey(): - # lets just create a sankey instance and check the code runs + # let's just create a sankey instance and check the code runs sankey = Sankey() sankey.add() diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index b3da951cf464..f98e083d84a0 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -6,8 +6,12 @@ LogTransform, InvertedLogTransform, SymmetricalLogTransform) import matplotlib.scale as mscale -from matplotlib.ticker import AsinhLocator, LogFormatterSciNotation +from matplotlib.ticker import ( + AsinhLocator, AutoLocator, LogFormatterSciNotation, + NullFormatter, NullLocator, ScalarFormatter +) from matplotlib.testing.decorators import check_figures_equal, image_comparison +from matplotlib.transforms import IdentityTransform import numpy as np from numpy.testing import assert_allclose @@ -295,3 +299,75 @@ def test_bad_scale(self): AsinhScale(axis=None, linear_width=-1) s0 = AsinhScale(axis=None, ) s1 = AsinhScale(axis=None, linear_width=3.0) + + +def test_custom_scale_without_axis(): + """ + Test that one can register and use custom scales that don't take an *axis* param. + """ + class CustomTransform(IdentityTransform): + pass + + class CustomScale(mscale.ScaleBase): + name = "custom" + + # Important: __init__ has no *axis* parameter + def __init__(self): + self._transform = CustomTransform() + + def get_transform(self): + return self._transform + + def set_default_locators_and_formatters(self, axis): + axis.set_major_locator(AutoLocator()) + axis.set_major_formatter(ScalarFormatter()) + axis.set_minor_locator(NullLocator()) + axis.set_minor_formatter(NullFormatter()) + + try: + mscale.register_scale(CustomScale) + fig, ax = plt.subplots() + ax.set_xscale('custom') + assert isinstance(ax.xaxis.get_transform(), CustomTransform) + finally: + # cleanup - there's no public unregister_scale() + del mscale._scale_mapping["custom"] + del mscale._scale_has_axis_parameter["custom"] + + +def test_custom_scale_with_axis(): + """ + Test that one can still register and use custom scales with an *axis* + parameter, but that registering issues a pending-deprecation warning. + """ + class CustomTransform(IdentityTransform): + pass + + class CustomScale(mscale.ScaleBase): + name = "custom" + + # Important: __init__ still has the *axis* parameter + def __init__(self, axis): + self._transform = CustomTransform() + + def get_transform(self): + return self._transform + + def set_default_locators_and_formatters(self, axis): + axis.set_major_locator(AutoLocator()) + axis.set_major_formatter(ScalarFormatter()) + axis.set_minor_locator(NullLocator()) + axis.set_minor_formatter(NullFormatter()) + + try: + with pytest.warns( + PendingDeprecationWarning, + match=r"'axis' parameter .* is pending-deprecated"): + mscale.register_scale(CustomScale) + fig, ax = plt.subplots() + ax.set_xscale('custom') + assert isinstance(ax.xaxis.get_transform(), CustomTransform) + finally: + # cleanup - there's no public unregister_scale() + del mscale._scale_mapping["custom"] + del mscale._scale_has_axis_parameter["custom"] diff --git a/lib/matplotlib/tests/test_skew.py b/lib/matplotlib/tests/test_skew.py index 8527e474fa21..125ecd7ff606 100644 --- a/lib/matplotlib/tests/test_skew.py +++ b/lib/matplotlib/tests/test_skew.py @@ -25,9 +25,9 @@ def draw(self, renderer): for artist in [self.gridline, self.tick1line, self.tick2line, self.label1, self.label2]: stack.callback(artist.set_visible, artist.get_visible()) - needs_lower = transforms.interval_contains( + needs_lower = transforms._interval_contains( self.axes.lower_xlim, self.get_loc()) - needs_upper = transforms.interval_contains( + needs_upper = transforms._interval_contains( self.axes.upper_xlim, self.get_loc()) self.tick1line.set_visible( self.tick1line.get_visible() and needs_lower) diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index ede3166a2e1b..c6f4e13c74c2 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -205,6 +205,30 @@ def test_plot_html_show_source_link_custom_basename(tmp_path): assert 'custom-name.py' in html_content +def test_plot_html_code_caption(tmp_path): + # Test that :code-caption: option adds caption to code block + shutil.copyfile(tinypages / 'conf.py', tmp_path / 'conf.py') + shutil.copytree(tinypages / '_static', tmp_path / '_static') + doctree_dir = tmp_path / 'doctrees' + (tmp_path / 'index.rst').write_text(""" +.. plot:: + :include-source: + :code-caption: Example plotting code + + import matplotlib.pyplot as plt + plt.plot([1, 2, 3], [1, 4, 9]) +""") + html_dir = tmp_path / '_build' / 'html' + build_sphinx_html(tmp_path, doctree_dir, html_dir) + + # Check that the HTML contains the code caption + html_content = (html_dir / 'index.html').read_text(encoding='utf-8') + assert 'Example plotting code' in html_content + # Verify the caption is associated with the code block + # (appears in a caption element) + assert '

info.misses +def test_metrics_cache2(): + # dig into the signature to get the mutable default used as a cache + renderer_cache = inspect.signature( + mpl.text._get_text_metrics_function + ).parameters['_cache'].default + gc.collect() + renderer_cache.clear() + + def helper(): + fig, ax = plt.subplots() + fig.draw_without_rendering() + # show we hit the outer cache + assert len(renderer_cache) == 1 + func = renderer_cache[fig.canvas.get_renderer()] + cache_info = func.cache_info() + # show we hit the inner cache + assert cache_info.currsize > 0 + assert cache_info.currsize == cache_info.misses + assert cache_info.hits > cache_info.misses + plt.close(fig) + + helper() + gc.collect() + # show the outer cache has a lifetime tied to the renderer (via the figure) + assert len(renderer_cache) == 0 + + def test_annotate_offset_fontsize(): # Test that offset_fontsize parameter works and uses accurate values fig, ax = plt.subplots() @@ -1096,8 +1135,9 @@ def test_empty_annotation_get_window_extent(): assert points[0, 1] == 50.0 +# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(baseline_images=['basictext_wrap'], - extensions=['png']) + extensions=['png'], tol=0.3) def test_basic_wrap(): fig = plt.figure() plt.axis([0, 10, 0, 10]) @@ -1113,8 +1153,9 @@ def test_basic_wrap(): plt.text(-1, 0, t, ha='left', rotation=-15, wrap=True) +# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(baseline_images=['fonttext_wrap'], - extensions=['png']) + extensions=['png'], tol=0.3) def test_font_wrap(): fig = plt.figure() plt.axis([0, 10, 0, 10]) @@ -1146,8 +1187,9 @@ def test_va_for_angle(): assert alignment in ['center', 'top', 'baseline'] +# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(baseline_images=['xtick_rotation_mode'], - remove_text=False, extensions=['png'], style='mpl20') + remove_text=False, extensions=['png'], style='mpl20', tol=0.3) def test_xtick_rotation_mode(): fig, ax = plt.subplots(figsize=(12, 1)) ax.set_yticks([]) @@ -1166,8 +1208,9 @@ def test_xtick_rotation_mode(): plt.subplots_adjust(left=0.01, right=0.99, top=.6, bottom=.4) +# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(baseline_images=['ytick_rotation_mode'], - remove_text=False, extensions=['png'], style='mpl20') + remove_text=False, extensions=['png'], style='mpl20', tol=0.3) def test_ytick_rotation_mode(): fig, ax = plt.subplots(figsize=(1, 12)) ax.set_xticks([]) diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 0f54230663aa..c3c53ebaea73 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -6,7 +6,7 @@ from packaging.version import parse as parse_version import numpy as np -from numpy.testing import assert_almost_equal, assert_array_equal +from numpy.testing import assert_almost_equal, assert_array_equal, assert_allclose import pytest import matplotlib as mpl @@ -356,6 +356,10 @@ def test_switch_to_autolocator(self): loc = mticker.LogLocator(subs=np.arange(2, 10)) assert 1.0 not in loc.tick_values(0.9, 20.) assert 10.0 not in loc.tick_values(0.9, 20.) + # don't switch if there's already one major and one minor tick (10 & 20) + loc = mticker.LogLocator(subs="auto") + tv = loc.tick_values(10, 20) + assert_array_equal(tv[(10 <= tv) & (tv <= 20)], [20]) def test_set_params(self): """ @@ -1931,7 +1935,10 @@ def test_bad_locator_subs(sub): @mpl.style.context('default') def test_small_range_loglocator(numticks, lims, ticks): ll = mticker.LogLocator(numticks=numticks) - assert_array_equal(ll.tick_values(*lims), ticks) + if parse_version(np.version.version).major < 2: + assert_allclose(ll.tick_values(*lims), ticks, rtol=2e-16) + else: + assert_array_equal(ll.tick_values(*lims), ticks) @mpl.style.context('default') diff --git a/lib/matplotlib/tests/test_transforms.py b/lib/matplotlib/tests/test_transforms.py index b4db34db5a91..2b4351a5cfbb 100644 --- a/lib/matplotlib/tests/test_transforms.py +++ b/lib/matplotlib/tests/test_transforms.py @@ -694,9 +694,9 @@ def test_contains_branch(self): assert not self.stack1.contains_branch(self.tn1 + self.ta2) blend = mtransforms.BlendedGenericTransform(self.tn2, self.stack2) - x, y = blend.contains_branch_seperately(self.stack2_subset) + x, y = blend.contains_branch_separately(self.stack2_subset) stack_blend = self.tn3 + blend - sx, sy = stack_blend.contains_branch_seperately(self.stack2_subset) + sx, sy = stack_blend.contains_branch_separately(self.stack2_subset) assert x is sx is False assert y is sy is True @@ -835,6 +835,16 @@ def assert_bbox_eq(bbox1, bbox2): assert_array_equal(bbox1.bounds, bbox2.bounds) +def test_bbox_is_finite(): + assert not Bbox([(1, 1), (1, 1)])._is_finite() + assert not Bbox([(0, 0), (np.inf, 1)])._is_finite() + assert not Bbox([(-np.inf, 0), (2, 2)])._is_finite() + assert not Bbox([(np.nan, 0), (2, 2)])._is_finite() + assert Bbox([(0, 0), (0, 2)])._is_finite() + assert Bbox([(0, 0), (2, 0)])._is_finite() + assert Bbox([(0, 0), (1, 2)])._is_finite() + + def test_bbox_frozen_copies_minpos(): bbox = mtransforms.Bbox.from_extents(0.0, 0.0, 1.0, 1.0, minpos=1.0) frozen = bbox.frozen() @@ -967,7 +977,7 @@ def test_nonsingular(): zero_expansion = np.array([-0.001, 0.001]) cases = [(0, np.nan), (0, 0), (0, 7.9e-317)] for args in cases: - out = np.array(mtransforms.nonsingular(*args)) + out = np.array(mtransforms._nonsingular(*args)) assert_array_equal(out, zero_expansion) @@ -1083,21 +1093,21 @@ def test_transformedbbox_contains(): def test_interval_contains(): - assert mtransforms.interval_contains((0, 1), 0.5) - assert mtransforms.interval_contains((0, 1), 0) - assert mtransforms.interval_contains((0, 1), 1) - assert not mtransforms.interval_contains((0, 1), -1) - assert not mtransforms.interval_contains((0, 1), 2) - assert mtransforms.interval_contains((1, 0), 0.5) + assert mtransforms._interval_contains((0, 1), 0.5) + assert mtransforms._interval_contains((0, 1), 0) + assert mtransforms._interval_contains((0, 1), 1) + assert not mtransforms._interval_contains((0, 1), -1) + assert not mtransforms._interval_contains((0, 1), 2) + assert mtransforms._interval_contains((1, 0), 0.5) def test_interval_contains_open(): - assert mtransforms.interval_contains_open((0, 1), 0.5) - assert not mtransforms.interval_contains_open((0, 1), 0) - assert not mtransforms.interval_contains_open((0, 1), 1) - assert not mtransforms.interval_contains_open((0, 1), -1) - assert not mtransforms.interval_contains_open((0, 1), 2) - assert mtransforms.interval_contains_open((1, 0), 0.5) + assert mtransforms._interval_contains_open((0, 1), 0.5) + assert not mtransforms._interval_contains_open((0, 1), 0) + assert not mtransforms._interval_contains_open((0, 1), 1) + assert not mtransforms._interval_contains_open((0, 1), -1) + assert not mtransforms._interval_contains_open((0, 1), 2) + assert mtransforms._interval_contains_open((1, 0), 0.5) def test_scaledrotation_initialization(): diff --git a/lib/matplotlib/tests/test_typing.py b/lib/matplotlib/tests/test_typing.py new file mode 100644 index 000000000000..c9fc8e5b162f --- /dev/null +++ b/lib/matplotlib/tests/test_typing.py @@ -0,0 +1,51 @@ +import re +import typing +from pathlib import Path + +import matplotlib.pyplot as plt +from matplotlib.colors import Colormap +from matplotlib.typing import RcKeyType, RcGroupKeyType + + +def test_cm_stub_matches_runtime_colormaps(): + runtime_cm = plt.cm + runtime_cmaps = { + name + for name, value in vars(runtime_cm).items() + if isinstance(value, Colormap) + } + + cm_pyi_path = Path(__file__).parent.parent / "cm.pyi" + assert cm_pyi_path.exists(), f"{cm_pyi_path} does not exist" + + pyi_content = cm_pyi_path.read_text(encoding='utf-8') + + stubbed_cmaps = set( + re.findall(r"^(\w+):\s+colors\.Colormap", pyi_content, re.MULTILINE) + ) + + assert runtime_cmaps, ( + "No colormaps variables found at runtime in matplotlib.colors" + ) + assert stubbed_cmaps, ( + "No colormaps found in cm.pyi" + ) + + assert runtime_cmaps == stubbed_cmaps + + +def test_rcparam_stubs(): + runtime_rc_keys = { + name for name in plt.rcParamsDefault.keys() + if not name.startswith('_') + } + + assert {*typing.get_args(RcKeyType)} == runtime_rc_keys + + runtime_rc_group_keys = set() + for name in runtime_rc_keys: + groups = name.split('.') + for i in range(1, len(groups)): + runtime_rc_group_keys.add('.'.join(groups[:i])) + + assert {*typing.get_args(RcGroupKeyType)} == runtime_rc_group_keys diff --git a/lib/matplotlib/tests/test_units.py b/lib/matplotlib/tests/test_units.py index d2350667e94f..c13c54a101fc 100644 --- a/lib/matplotlib/tests/test_units.py +++ b/lib/matplotlib/tests/test_units.py @@ -80,8 +80,9 @@ def default_units(value, axis): # Tests that the conversion machinery works properly for classes that # work as a facade over numpy arrays (like pint) +# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(['plot_pint.png'], style='mpl20', - tol=0 if platform.machine() == 'x86_64' else 0.03) + tol=0.03 if platform.machine() == 'x86_64' else 0.04) def test_numpy_facade(quantity_converter): # use former defaults to match existing baseline image plt.rcParams['axes.formatter.limits'] = -7, 7 @@ -142,8 +143,9 @@ def test_jpl_bar_units(): ax.set_ylim([b - 1 * day, b + w[-1] + (1.001) * day]) +# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(['jpl_barh_units.png'], - savefig_kwarg={'dpi': 120}, style='mpl20') + savefig_kwarg={'dpi': 120}, style='mpl20', tol=0.02) def test_jpl_barh_units(): import matplotlib.testing.jpl_units as units units.register() diff --git a/lib/matplotlib/tests/test_usetex.py b/lib/matplotlib/tests/test_usetex.py index cd9f2597361b..78d9fd6cc948 100644 --- a/lib/matplotlib/tests/test_usetex.py +++ b/lib/matplotlib/tests/test_usetex.py @@ -226,8 +226,9 @@ def test_pdf_type1_font_subsetting(): _old_gs_version = True +# TODO: tighten tolerance after baseline image is regenerated for text overhaul @image_comparison(baseline_images=['rotation'], extensions=['eps', 'pdf', 'png', 'svg'], - style='mpl20', tol=3.91 if _old_gs_version else 0) + style='mpl20', tol=3.91 if _old_gs_version else 0.2) def test_rotation(): mpl.rcParams['text.usetex'] = True diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 808863fd6a94..9eebf165e71f 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -3,13 +3,13 @@ import operator from unittest import mock -from matplotlib.backend_bases import MouseEvent +import matplotlib as mpl +from matplotlib.backend_bases import DrawEvent, KeyEvent, MouseEvent import matplotlib.colors as mcolors import matplotlib.widgets as widgets import matplotlib.pyplot as plt from matplotlib.testing.decorators import check_figures_equal, image_comparison -from matplotlib.testing.widgets import (click_and_drag, do_event, get_ax, - mock_event, noop) +from matplotlib.testing.widgets import click_and_drag, get_ax, noop import numpy as np from numpy.testing import assert_allclose @@ -71,11 +71,10 @@ def test_rectangle_selector(ax, kwargs): onselect = mock.Mock(spec=noop, return_value=None) 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) - + MouseEvent._from_ax_coords("button_press_event", ax, (100, 100), 1)._process() + MouseEvent._from_ax_coords("motion_notify_event", ax, (199, 199), 1)._process() # purposely drag outside of axis for release - do_event(tool, 'release', xdata=250, ydata=250, button=1) + MouseEvent._from_ax_coords("button_release_event", ax, (250, 250), 1)._process() if kwargs.get('drawtype', None) not in ['line', 'none']: assert_allclose(tool.geometry, @@ -137,7 +136,7 @@ def test_rectangle_drag(ax, drag_from_anywhere, new_center): tool = widgets.RectangleSelector(ax, interactive=True, drag_from_anywhere=drag_from_anywhere) # Create rectangle - click_and_drag(tool, start=(0, 10), end=(100, 120)) + click_and_drag(tool, start=(10, 10), end=(90, 120)) assert tool.center == (50, 65) # Drag inside rectangle, but away from centre handle # @@ -178,8 +177,8 @@ def test_rectangle_selector_set_props_handle_props(ax): def test_rectangle_resize(ax): 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) + click_and_drag(tool, start=(10, 10), end=(100, 120)) + assert tool.extents == (10.0, 100.0, 10.0, 120.0) # resize NE handle extents = tool.extents @@ -446,11 +445,11 @@ def test_rectangle_rotate(ax, selector_class): assert len(tool._state) == 0 # Rotate anticlockwise using top-right corner - do_event(tool, 'on_key_press', key='r') + KeyEvent("key_press_event", ax.figure.canvas, "r")._process() assert tool._state == {'rotate'} assert len(tool._state) == 1 click_and_drag(tool, start=(130, 140), end=(120, 145)) - do_event(tool, 'on_key_press', key='r') + KeyEvent("key_press_event", ax.figure.canvas, "r")._process() assert len(tool._state) == 0 # Extents shouldn't change (as shape of rectangle hasn't changed) assert tool.extents == (100, 130, 100, 140) @@ -623,27 +622,36 @@ def test_rectangle_selector_ignore_outside(ax, ignore_event_outside): ('horizontal', False, dict(interactive=True)), ]) def test_span_selector(ax, orientation, onmove_callback, kwargs): - onselect = mock.Mock(spec=noop, return_value=None) - onmove = mock.Mock(spec=noop, return_value=None) - if onmove_callback: - kwargs['onmove_callback'] = onmove - - # While at it, also test that span selectors work in the presence of twin axes on - # top of the axes that contain the selector. Note that we need to unforce the axes - # aspect here, otherwise the twin axes forces the original axes' limits (to respect - # aspect=1) which makes some of the values below go out of bounds. + # Also test that span selectors work in the presence of twin axes or for + # outside-inset axes on top of the axes that contain the selector. Note + # that we need to unforce the axes aspect here, otherwise the twin axes + # forces the original axes' limits (to respect aspect=1) which makes some + # of the values below go out of bounds. ax.set_aspect("auto") - tax = ax.twinx() - - tool = widgets.SpanSelector(ax, onselect, orientation, **kwargs) - do_event(tool, 'press', xdata=100, ydata=100, button=1) - # move outside of axis - do_event(tool, 'onmove', xdata=199, ydata=199, button=1) - do_event(tool, 'release', xdata=250, ydata=250, button=1) - - onselect.assert_called_once_with(100, 199) - if onmove_callback: - onmove.assert_called_once_with(100, 199) + ax.twinx() + child = ax.inset_axes([0, 1, 1, 1], xlim=(0, 200), ylim=(0, 200)) + + for target in [ax, child]: + selected = [] + def onselect(*args): selected.append(args) + moved = [] + def onmove(*args): moved.append(args) + if onmove_callback: + kwargs['onmove_callback'] = onmove + + tool = widgets.SpanSelector(target, onselect, orientation, **kwargs) + MouseEvent._from_ax_coords( + "button_press_event", target, (100, 100), 1)._process() + # move outside of axis + MouseEvent._from_ax_coords( + "motion_notify_event", target, (199, 199), 1)._process() + MouseEvent._from_ax_coords( + "button_release_event", target, (250, 250), 1)._process() + + # tol is set by pixel size (~100 pixels & span of 200 data units) + assert_allclose(selected, [(100, 199)], atol=.5) + if onmove_callback: + assert_allclose(moved, [(100, 199)], atol=.5) @pytest.mark.parametrize('interactive', [True, False]) @@ -783,7 +791,7 @@ def test_selector_clear(ax, selector): click_and_drag(tool, start=(130, 130), end=(130, 130)) assert tool._selection_completed - do_event(tool, 'on_key_press', key='escape') + KeyEvent("key_press_event", ax.figure.canvas, "escape")._process() assert not tool._selection_completed @@ -905,10 +913,8 @@ def mean(vmin, vmax): # Add span selector and check that the line is draw after it was updated # by the callback - press_data = [1, 2] - move_data = [2, 2] - do_event(span, 'press', xdata=press_data[0], ydata=press_data[1], button=1) - do_event(span, 'onmove', xdata=move_data[0], ydata=move_data[1], button=1) + MouseEvent._from_ax_coords("button_press_event", ax, (1, 2), 1)._process() + MouseEvent._from_ax_coords("motion_notify_event", ax, (2, 2), 1)._process() assert span._get_animated_artists() == (ln, ln2) assert ln.stale is False assert ln2.stale @@ -918,16 +924,12 @@ def mean(vmin, vmax): # Change span selector and check that the line is drawn/updated after its # value was updated by the callback - press_data = [4, 0] - move_data = [5, 2] - release_data = [5, 2] - do_event(span, 'press', xdata=press_data[0], ydata=press_data[1], button=1) - do_event(span, 'onmove', xdata=move_data[0], ydata=move_data[1], button=1) + MouseEvent._from_ax_coords("button_press_event", ax, (4, 0), 1)._process() + MouseEvent._from_ax_coords("motion_notify_event", ax, (5, 2), 1)._process() assert ln.stale is False assert ln2.stale assert_allclose(ln2.get_ydata(), -0.9424150707548072) - do_event(span, 'release', xdata=release_data[0], - ydata=release_data[1], button=1) + MouseEvent._from_ax_coords("button_release_event", ax, (5, 2), 1)._process() assert ln2.stale is False @@ -988,9 +990,9 @@ def test_lasso_selector(ax, kwargs): onselect = mock.Mock(spec=noop, return_value=None) 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) + MouseEvent._from_ax_coords("button_press_event", ax, (100, 100), 1)._process() + MouseEvent._from_ax_coords("motion_notify_event", ax, (125, 125), 1)._process() + MouseEvent._from_ax_coords("button_release_event", ax, (150, 150), 1)._process() onselect.assert_called_once_with([(100, 100), (125, 125), (150, 150)]) @@ -1066,7 +1068,7 @@ def test_TextBox(ax, toolbar): assert tool.text == '' - do_event(tool, '_click') + MouseEvent._from_ax_coords("button_press_event", ax, (.5, .5), 1)._process() tool.set_val('x**2') @@ -1078,9 +1080,9 @@ def test_TextBox(ax, toolbar): assert submit_event.call_count == 2 - do_event(tool, '_click', xdata=.5, ydata=.5) # Ensure the click is in the axes. - do_event(tool, '_keypress', key='+') - do_event(tool, '_keypress', key='5') + MouseEvent._from_ax_coords("button_press_event", ax, (.5, .5), 1)._process() + KeyEvent("key_press_event", ax.figure.canvas, "+")._process() + KeyEvent("key_press_event", ax.figure.canvas, "5")._process() assert text_change_event.call_count == 3 @@ -1343,162 +1345,160 @@ def test_range_slider_same_init_values(orientation): assert_allclose(box.get_points().flatten()[idx], [0, 0.25, 0, 0.75]) -def check_polygon_selector(event_sequence, expected_result, selections_count, - **kwargs): +def check_polygon_selector(events, expected, selections_count, **kwargs): """ Helper function to test Polygon Selector. Parameters ---------- - event_sequence : list of tuples (etype, dict()) - A sequence of events to perform. The sequence is a list of tuples - where the first element of the tuple is an etype (e.g., 'onmove', - 'press', etc.), and the second element of the tuple is a dictionary of - the arguments for the event (e.g., xdata=5, key='shift', etc.). - expected_result : list of vertices (xdata, ydata) - The list of vertices that are expected to result from the event - sequence. + events : list[MouseEvent] + A sequence of events to perform. + expected : list of vertices (xdata, ydata) + The list of vertices expected to result from the event sequence. selections_count : int Wait for the tool to call its `onselect` function `selections_count` - times, before comparing the result to the `expected_result` + times, before comparing the result to the `expected` **kwargs Keyword arguments are passed to PolygonSelector. """ - ax = get_ax() - onselect = mock.Mock(spec=noop, return_value=None) + ax = events[0].canvas.figure.axes[0] tool = widgets.PolygonSelector(ax, onselect=onselect, **kwargs) - for (etype, event_args) in event_sequence: - do_event(tool, etype, **event_args) + for event in events: + event._process() assert onselect.call_count == selections_count - assert onselect.call_args == ((expected_result, ), {}) + assert onselect.call_args == ((expected, ), {}) -def polygon_place_vertex(xdata, ydata): - return [('onmove', dict(xdata=xdata, ydata=ydata)), - ('press', dict(xdata=xdata, ydata=ydata)), - ('release', dict(xdata=xdata, ydata=ydata))] +def polygon_place_vertex(ax, xy): + return [ + MouseEvent._from_ax_coords("motion_notify_event", ax, xy), + MouseEvent._from_ax_coords("button_press_event", ax, xy, 1), + MouseEvent._from_ax_coords("button_release_event", ax, xy, 1), + ] -def polygon_remove_vertex(xdata, ydata): - return [('onmove', dict(xdata=xdata, ydata=ydata)), - ('press', dict(xdata=xdata, ydata=ydata, button=3)), - ('release', dict(xdata=xdata, ydata=ydata, button=3))] +def polygon_remove_vertex(ax, xy): + return [ + MouseEvent._from_ax_coords("motion_notify_event", ax, xy), + MouseEvent._from_ax_coords("button_press_event", ax, xy, 3), + MouseEvent._from_ax_coords("button_release_event", ax, xy, 3), + ] @pytest.mark.parametrize('draw_bounding_box', [False, True]) -def test_polygon_selector(draw_bounding_box): +def test_polygon_selector(ax, draw_bounding_box): check_selector = functools.partial( check_polygon_selector, draw_bounding_box=draw_bounding_box) # Simple polygon expected_result = [(50, 50), (150, 50), (50, 150)] event_sequence = [ - *polygon_place_vertex(50, 50), - *polygon_place_vertex(150, 50), - *polygon_place_vertex(50, 150), - *polygon_place_vertex(50, 50), + *polygon_place_vertex(ax, (50, 50)), + *polygon_place_vertex(ax, (150, 50)), + *polygon_place_vertex(ax, (50, 150)), + *polygon_place_vertex(ax, (50, 50)), ] check_selector(event_sequence, expected_result, 1) # Move first vertex before completing the polygon. expected_result = [(75, 50), (150, 50), (50, 150)] event_sequence = [ - *polygon_place_vertex(50, 50), - *polygon_place_vertex(150, 50), - ('on_key_press', dict(key='control')), - ('onmove', dict(xdata=50, ydata=50)), - ('press', dict(xdata=50, ydata=50)), - ('onmove', dict(xdata=75, ydata=50)), - ('release', dict(xdata=75, ydata=50)), - ('on_key_release', dict(key='control')), - *polygon_place_vertex(50, 150), - *polygon_place_vertex(75, 50), + *polygon_place_vertex(ax, (50, 50)), + *polygon_place_vertex(ax, (150, 50)), + KeyEvent("key_press_event", ax.figure.canvas, "control"), + MouseEvent._from_ax_coords("motion_notify_event", ax, (50, 50)), + MouseEvent._from_ax_coords("button_press_event", ax, (50, 50), 1), + MouseEvent._from_ax_coords("motion_notify_event", ax, (75, 50)), + MouseEvent._from_ax_coords("button_release_event", ax, (75, 50), 1), + KeyEvent("key_release_event", ax.figure.canvas, "control"), + *polygon_place_vertex(ax, (50, 150)), + *polygon_place_vertex(ax, (75, 50)), ] check_selector(event_sequence, expected_result, 1) # Move first two vertices at once before completing the polygon. expected_result = [(50, 75), (150, 75), (50, 150)] event_sequence = [ - *polygon_place_vertex(50, 50), - *polygon_place_vertex(150, 50), - ('on_key_press', dict(key='shift')), - ('onmove', dict(xdata=100, ydata=100)), - ('press', dict(xdata=100, ydata=100)), - ('onmove', dict(xdata=100, ydata=125)), - ('release', dict(xdata=100, ydata=125)), - ('on_key_release', dict(key='shift')), - *polygon_place_vertex(50, 150), - *polygon_place_vertex(50, 75), + *polygon_place_vertex(ax, (50, 50)), + *polygon_place_vertex(ax, (150, 50)), + KeyEvent("key_press_event", ax.figure.canvas, "shift"), + MouseEvent._from_ax_coords("motion_notify_event", ax, (100, 100)), + MouseEvent._from_ax_coords("button_press_event", ax, (100, 100), 1), + MouseEvent._from_ax_coords("motion_notify_event", ax, (100, 125)), + MouseEvent._from_ax_coords("button_release_event", ax, (100, 125), 1), + KeyEvent("key_release_event", ax.figure.canvas, "shift"), + *polygon_place_vertex(ax, (50, 150)), + *polygon_place_vertex(ax, (50, 75)), ] check_selector(event_sequence, expected_result, 1) # Move first vertex after completing the polygon. - expected_result = [(75, 50), (150, 50), (50, 150)] + expected_result = [(85, 50), (150, 50), (50, 150)] event_sequence = [ - *polygon_place_vertex(50, 50), - *polygon_place_vertex(150, 50), - *polygon_place_vertex(50, 150), - *polygon_place_vertex(50, 50), - ('onmove', dict(xdata=50, ydata=50)), - ('press', dict(xdata=50, ydata=50)), - ('onmove', dict(xdata=75, ydata=50)), - ('release', dict(xdata=75, ydata=50)), + *polygon_place_vertex(ax, (60, 50)), + *polygon_place_vertex(ax, (150, 50)), + *polygon_place_vertex(ax, (50, 150)), + *polygon_place_vertex(ax, (60, 50)), + MouseEvent._from_ax_coords("motion_notify_event", ax, (60, 50)), + MouseEvent._from_ax_coords("button_press_event", ax, (60, 50), 1), + MouseEvent._from_ax_coords("motion_notify_event", ax, (85, 50)), + MouseEvent._from_ax_coords("button_release_event", ax, (85, 50), 1), ] check_selector(event_sequence, expected_result, 2) # Move all vertices after completing the polygon. expected_result = [(75, 75), (175, 75), (75, 175)] event_sequence = [ - *polygon_place_vertex(50, 50), - *polygon_place_vertex(150, 50), - *polygon_place_vertex(50, 150), - *polygon_place_vertex(50, 50), - ('on_key_press', dict(key='shift')), - ('onmove', dict(xdata=100, ydata=100)), - ('press', dict(xdata=100, ydata=100)), - ('onmove', dict(xdata=125, ydata=125)), - ('release', dict(xdata=125, ydata=125)), - ('on_key_release', dict(key='shift')), + *polygon_place_vertex(ax, (50, 50)), + *polygon_place_vertex(ax, (150, 50)), + *polygon_place_vertex(ax, (50, 150)), + *polygon_place_vertex(ax, (50, 50)), + KeyEvent("key_press_event", ax.figure.canvas, "shift"), + MouseEvent._from_ax_coords("motion_notify_event", ax, (100, 100)), + MouseEvent._from_ax_coords("button_press_event", ax, (100, 100), 1), + MouseEvent._from_ax_coords("motion_notify_event", ax, (125, 125)), + MouseEvent._from_ax_coords("button_release_event", ax, (125, 125), 1), + KeyEvent("key_release_event", ax.figure.canvas, "shift"), ] check_selector(event_sequence, expected_result, 2) # Try to move a vertex and move all before placing any vertices. expected_result = [(50, 50), (150, 50), (50, 150)] event_sequence = [ - ('on_key_press', dict(key='control')), - ('onmove', dict(xdata=100, ydata=100)), - ('press', dict(xdata=100, ydata=100)), - ('onmove', dict(xdata=125, ydata=125)), - ('release', dict(xdata=125, ydata=125)), - ('on_key_release', dict(key='control')), - ('on_key_press', dict(key='shift')), - ('onmove', dict(xdata=100, ydata=100)), - ('press', dict(xdata=100, ydata=100)), - ('onmove', dict(xdata=125, ydata=125)), - ('release', dict(xdata=125, ydata=125)), - ('on_key_release', dict(key='shift')), - *polygon_place_vertex(50, 50), - *polygon_place_vertex(150, 50), - *polygon_place_vertex(50, 150), - *polygon_place_vertex(50, 50), + KeyEvent("key_press_event", ax.figure.canvas, "control"), + MouseEvent._from_ax_coords("motion_notify_event", ax, (100, 100)), + MouseEvent._from_ax_coords("button_press_event", ax, (100, 100), 1), + MouseEvent._from_ax_coords("motion_notify_event", ax, (125, 125)), + MouseEvent._from_ax_coords("button_release_event", ax, (125, 125), 1), + KeyEvent("key_release_event", ax.figure.canvas, "control"), + KeyEvent("key_press_event", ax.figure.canvas, "shift"), + MouseEvent._from_ax_coords("motion_notify_event", ax, (100, 100)), + MouseEvent._from_ax_coords("button_press_event", ax, (100, 100), 1), + MouseEvent._from_ax_coords("motion_notify_event", ax, (125, 125)), + MouseEvent._from_ax_coords("button_release_event", ax, (125, 125), 1), + KeyEvent("key_release_event", ax.figure.canvas, "shift"), + *polygon_place_vertex(ax, (50, 50)), + *polygon_place_vertex(ax, (150, 50)), + *polygon_place_vertex(ax, (50, 150)), + *polygon_place_vertex(ax, (50, 50)), ] check_selector(event_sequence, expected_result, 1) # Try to place vertex out-of-bounds, then reset, and start a new polygon. expected_result = [(50, 50), (150, 50), (50, 150)] event_sequence = [ - *polygon_place_vertex(50, 50), - *polygon_place_vertex(250, 50), - ('on_key_press', dict(key='escape')), - ('on_key_release', dict(key='escape')), - *polygon_place_vertex(50, 50), - *polygon_place_vertex(150, 50), - *polygon_place_vertex(50, 150), - *polygon_place_vertex(50, 50), + *polygon_place_vertex(ax, (50, 50)), + *polygon_place_vertex(ax, (250, 50)), + KeyEvent("key_press_event", ax.figure.canvas, "escape"), + KeyEvent("key_release_event", ax.figure.canvas, "escape"), + *polygon_place_vertex(ax, (50, 50)), + *polygon_place_vertex(ax, (150, 50)), + *polygon_place_vertex(ax, (50, 150)), + *polygon_place_vertex(ax, (50, 50)), ] check_selector(event_sequence, expected_result, 1) @@ -1510,15 +1510,13 @@ def test_polygon_selector_set_props_handle_props(ax, draw_bounding_box): handle_props=dict(alpha=0.5), draw_bounding_box=draw_bounding_box) - event_sequence = [ - *polygon_place_vertex(50, 50), - *polygon_place_vertex(150, 50), - *polygon_place_vertex(50, 150), - *polygon_place_vertex(50, 50), - ] - - for (etype, event_args) in event_sequence: - do_event(tool, etype, **event_args) + for event in [ + *polygon_place_vertex(ax, (50, 50)), + *polygon_place_vertex(ax, (150, 50)), + *polygon_place_vertex(ax, (50, 150)), + *polygon_place_vertex(ax, (50, 50)), + ]: + event._process() artist = tool._selection_artist assert artist.get_color() == 'b' @@ -1549,17 +1547,17 @@ def test_rect_visibility(fig_test, fig_ref): # Change the order that the extra point is inserted in @pytest.mark.parametrize('idx', [1, 2, 3]) @pytest.mark.parametrize('draw_bounding_box', [False, True]) -def test_polygon_selector_remove(idx, draw_bounding_box): +def test_polygon_selector_remove(ax, idx, draw_bounding_box): verts = [(50, 50), (150, 50), (50, 150)] - event_sequence = [polygon_place_vertex(*verts[0]), - polygon_place_vertex(*verts[1]), - polygon_place_vertex(*verts[2]), + event_sequence = [polygon_place_vertex(ax, verts[0]), + polygon_place_vertex(ax, verts[1]), + polygon_place_vertex(ax, verts[2]), # Finish the polygon - polygon_place_vertex(*verts[0])] + polygon_place_vertex(ax, verts[0])] # Add an extra point - event_sequence.insert(idx, polygon_place_vertex(200, 200)) + event_sequence.insert(idx, polygon_place_vertex(ax, (200, 200))) # Remove the extra point - event_sequence.append(polygon_remove_vertex(200, 200)) + event_sequence.append(polygon_remove_vertex(ax, (200, 200))) # Flatten list of lists event_sequence = functools.reduce(operator.iadd, event_sequence, []) check_polygon_selector(event_sequence, verts, 2, @@ -1567,14 +1565,14 @@ def test_polygon_selector_remove(idx, draw_bounding_box): @pytest.mark.parametrize('draw_bounding_box', [False, True]) -def test_polygon_selector_remove_first_point(draw_bounding_box): +def test_polygon_selector_remove_first_point(ax, draw_bounding_box): verts = [(50, 50), (150, 50), (50, 150)] event_sequence = [ - *polygon_place_vertex(*verts[0]), - *polygon_place_vertex(*verts[1]), - *polygon_place_vertex(*verts[2]), - *polygon_place_vertex(*verts[0]), - *polygon_remove_vertex(*verts[0]), + *polygon_place_vertex(ax, verts[0]), + *polygon_place_vertex(ax, verts[1]), + *polygon_place_vertex(ax, verts[2]), + *polygon_place_vertex(ax, verts[0]), + *polygon_remove_vertex(ax, verts[0]), ] check_polygon_selector(event_sequence, verts[1:], 2, draw_bounding_box=draw_bounding_box) @@ -1584,20 +1582,20 @@ def test_polygon_selector_remove_first_point(draw_bounding_box): def test_polygon_selector_redraw(ax, draw_bounding_box): verts = [(50, 50), (150, 50), (50, 150)] event_sequence = [ - *polygon_place_vertex(*verts[0]), - *polygon_place_vertex(*verts[1]), - *polygon_place_vertex(*verts[2]), - *polygon_place_vertex(*verts[0]), + *polygon_place_vertex(ax, verts[0]), + *polygon_place_vertex(ax, verts[1]), + *polygon_place_vertex(ax, verts[2]), + *polygon_place_vertex(ax, verts[0]), # Polygon completed, now remove first two verts. - *polygon_remove_vertex(*verts[1]), - *polygon_remove_vertex(*verts[2]), + *polygon_remove_vertex(ax, verts[1]), + *polygon_remove_vertex(ax, verts[2]), # At this point the tool should be reset so we can add more vertices. - *polygon_place_vertex(*verts[1]), + *polygon_place_vertex(ax, verts[1]), ] tool = widgets.PolygonSelector(ax, draw_bounding_box=draw_bounding_box) - for (etype, event_args) in event_sequence: - do_event(tool, etype, **event_args) + for event in event_sequence: + event._process() # After removing two verts, only one remains, and the # selector should be automatically reset assert tool.verts == verts[0:2] @@ -1615,14 +1613,13 @@ def test_polygon_selector_verts_setter(fig_test, fig_ref, draw_bounding_box): ax_ref = fig_ref.add_subplot() tool_ref = widgets.PolygonSelector(ax_ref, draw_bounding_box=draw_bounding_box) - event_sequence = [ - *polygon_place_vertex(*verts[0]), - *polygon_place_vertex(*verts[1]), - *polygon_place_vertex(*verts[2]), - *polygon_place_vertex(*verts[0]), - ] - for (etype, event_args) in event_sequence: - do_event(tool_ref, etype, **event_args) + for event in [ + *polygon_place_vertex(ax_ref, verts[0]), + *polygon_place_vertex(ax_ref, verts[1]), + *polygon_place_vertex(ax_ref, verts[2]), + *polygon_place_vertex(ax_ref, verts[0]), + ]: + event._process() def test_polygon_selector_box(ax): @@ -1630,40 +1627,29 @@ def test_polygon_selector_box(ax): ax.set(xlim=(-10, 50), ylim=(-10, 50)) verts = [(20, 0), (0, 20), (20, 40), (40, 20)] event_sequence = [ - *polygon_place_vertex(*verts[0]), - *polygon_place_vertex(*verts[1]), - *polygon_place_vertex(*verts[2]), - *polygon_place_vertex(*verts[3]), - *polygon_place_vertex(*verts[0]), + *polygon_place_vertex(ax, verts[0]), + *polygon_place_vertex(ax, verts[1]), + *polygon_place_vertex(ax, verts[2]), + *polygon_place_vertex(ax, verts[3]), + *polygon_place_vertex(ax, verts[0]), ] # Create selector tool = widgets.PolygonSelector(ax, draw_bounding_box=True) - for (etype, event_args) in event_sequence: - do_event(tool, etype, **event_args) - - # In order to trigger the correct callbacks, trigger events on the canvas - # instead of the individual tools - t = ax.transData - canvas = ax.get_figure(root=True).canvas + for event in event_sequence: + event._process() # Scale to half size using the top right corner of the bounding box - MouseEvent( - "button_press_event", canvas, *t.transform((40, 40)), 1)._process() - MouseEvent( - "motion_notify_event", canvas, *t.transform((20, 20)))._process() - MouseEvent( - "button_release_event", canvas, *t.transform((20, 20)), 1)._process() + MouseEvent._from_ax_coords("button_press_event", ax, (40, 40), 1)._process() + MouseEvent._from_ax_coords("motion_notify_event", ax, (20, 20))._process() + MouseEvent._from_ax_coords("button_release_event", ax, (20, 20), 1)._process() np.testing.assert_allclose( tool.verts, [(10, 0), (0, 10), (10, 20), (20, 10)]) # Move using the center of the bounding box - MouseEvent( - "button_press_event", canvas, *t.transform((10, 10)), 1)._process() - MouseEvent( - "motion_notify_event", canvas, *t.transform((30, 30)))._process() - MouseEvent( - "button_release_event", canvas, *t.transform((30, 30)), 1)._process() + MouseEvent._from_ax_coords("button_press_event", ax, (10, 10), 1)._process() + MouseEvent._from_ax_coords("motion_notify_event", ax, (30, 30))._process() + MouseEvent._from_ax_coords("button_release_event", ax, (30, 30), 1)._process() np.testing.assert_allclose( tool.verts, [(30, 20), (20, 30), (30, 40), (40, 30)]) @@ -1671,10 +1657,8 @@ def test_polygon_selector_box(ax): np.testing.assert_allclose( tool._box.extents, (20.0, 40.0, 20.0, 40.0)) - MouseEvent( - "button_press_event", canvas, *t.transform((30, 20)), 3)._process() - MouseEvent( - "button_release_event", canvas, *t.transform((30, 20)), 3)._process() + MouseEvent._from_ax_coords("button_press_event", ax, (30, 20), 3)._process() + MouseEvent._from_ax_coords("button_release_event", ax, (30, 20), 3)._process() np.testing.assert_allclose( tool.verts, [(20, 30), (30, 40), (40, 30)]) np.testing.assert_allclose( @@ -1687,9 +1671,9 @@ def test_polygon_selector_clear_method(ax): for result in ([(50, 50), (150, 50), (50, 150), (50, 50)], [(50, 50), (100, 50), (50, 150), (50, 50)]): - for x, y in result: - for etype, event_args in polygon_place_vertex(x, y): - do_event(tool, etype, **event_args) + for xy in result: + for event in polygon_place_vertex(ax, xy): + event._process() artist = tool._selection_artist @@ -1706,25 +1690,28 @@ def test_polygon_selector_clear_method(ax): @pytest.mark.parametrize("horizOn", [False, True]) @pytest.mark.parametrize("vertOn", [False, True]) -def test_MultiCursor(horizOn, vertOn): +@pytest.mark.parametrize("with_deprecated_canvas", [False, True]) +def test_MultiCursor(horizOn, vertOn, with_deprecated_canvas): fig = plt.figure() (ax1, ax3) = fig.subplots(2, sharex=True) ax2 = plt.figure().subplots() - # useblit=false to avoid having to draw the figure to cache the renderer - multi = widgets.MultiCursor( - None, (ax1, ax2), useblit=False, horizOn=horizOn, vertOn=vertOn - ) + if with_deprecated_canvas: + with pytest.warns(mpl.MatplotlibDeprecationWarning, match=r"canvas.*deprecat"): + multi = widgets.MultiCursor( + None, (ax1, ax2), useblit=False, horizOn=horizOn, vertOn=vertOn + ) + else: + # useblit=false to avoid having to draw the figure to cache the renderer + multi = widgets.MultiCursor( + (ax1, ax2), useblit=False, horizOn=horizOn, vertOn=vertOn + ) # Only two of the axes should have a line drawn on them. assert len(multi.vlines) == 2 assert len(multi.hlines) == 2 - # mock a motion_notify_event - # Can't use `do_event` as that helper requires the widget - # to have a single .ax attribute. - event = mock_event(ax1, xdata=.5, ydata=.25) - multi.onmove(event) + MouseEvent._from_ax_coords("motion_notify_event", ax1, (.5, .25))._process() # force a draw + draw event to exercise clear fig.canvas.draw() @@ -1742,8 +1729,7 @@ def test_MultiCursor(horizOn, vertOn): # After toggling settings, the opposite lines should be visible after move. multi.horizOn = not multi.horizOn multi.vertOn = not multi.vertOn - event = mock_event(ax1, xdata=.5, ydata=.25) - multi.onmove(event) + MouseEvent._from_ax_coords("motion_notify_event", ax1, (.5, .25))._process() assert len([line for line in multi.vlines if line.get_visible()]) == ( 0 if vertOn else 2) assert len([line for line in multi.hlines if line.get_visible()]) == ( @@ -1751,9 +1737,31 @@ def test_MultiCursor(horizOn, vertOn): # test a move event in an Axes not part of the MultiCursor # the lines in ax1 and ax2 should not have moved. - event = mock_event(ax3, xdata=.75, ydata=.75) - multi.onmove(event) + MouseEvent._from_ax_coords("motion_notify_event", ax3, (.75, .75))._process() for l in multi.vlines: assert l.get_xdata() == (.5, .5) for l in multi.hlines: assert l.get_ydata() == (.25, .25) + + +def test_parent_axes_removal(): + + fig, (ax_radio, ax_checks) = plt.subplots(1, 2) + + radio = widgets.RadioButtons(ax_radio, ['1', '2'], 0) + checks = widgets.CheckButtons(ax_checks, ['1', '2'], [True, False]) + + ax_checks.remove() + ax_radio.remove() + with io.BytesIO() as out: + # verify that saving does not raise + fig.savefig(out, format='raw') + + # verify that this method which is triggered by a draw_event callback when + # blitting is enabled does not raise. Calling private methods is simpler + # than trying to force blitting to be enabled with Agg or use a GUI + # framework. + renderer = fig._get_renderer() + evt = DrawEvent('draw_event', fig.canvas, renderer) + radio._clear(evt) + checks._clear(evt) diff --git a/lib/matplotlib/texmanager.py b/lib/matplotlib/texmanager.py index 020a26e31cbe..35651a94aa85 100644 --- a/lib/matplotlib/texmanager.py +++ b/lib/matplotlib/texmanager.py @@ -23,7 +23,6 @@ import functools import hashlib import logging -import os from pathlib import Path import subprocess from tempfile import TemporaryDirectory @@ -63,7 +62,7 @@ class TexManager: Repeated calls to this constructor always return the same instance. """ - _texcache = os.path.join(mpl.get_cachedir(), 'tex.cache') + _cache_dir = Path(mpl.get_cachedir(), 'tex.cache') _grey_arrayd = {} _font_families = ('serif', 'sans-serif', 'cursive', 'monospace') @@ -109,7 +108,7 @@ class TexManager: @functools.lru_cache # Always return the same instance. def __new__(cls): - Path(cls._texcache).mkdir(parents=True, exist_ok=True) + cls._cache_dir.mkdir(parents=True, exist_ok=True) return object.__new__(cls) @classmethod @@ -167,23 +166,30 @@ def _get_font_preamble_and_command(cls): return preamble, fontcmd @classmethod - def get_basefile(cls, tex, fontsize, dpi=None): + def _get_base_path(cls, tex, fontsize, dpi=None): """ - Return a filename based on a hash of the string, fontsize, and dpi. + Return a file path based on a hash of the string, fontsize, and dpi. """ src = cls._get_tex_source(tex, fontsize) + str(dpi) filehash = hashlib.sha256( src.encode('utf-8'), usedforsecurity=False ).hexdigest() - filepath = Path(cls._texcache) + filepath = cls._cache_dir num_letters, num_levels = 2, 2 for i in range(0, num_letters*num_levels, num_letters): - filepath = filepath / Path(filehash[i:i+2]) + filepath = filepath / filehash[i:i+2] filepath.mkdir(parents=True, exist_ok=True) - return os.path.join(filepath, filehash) + return filepath / filehash + + @classmethod + def get_basefile(cls, tex, fontsize, dpi=None): # Kept for backcompat. + """ + Return a filename based on a hash of the string, fontsize, and dpi. + """ + return str(cls._get_base_path(tex, fontsize, dpi)) @classmethod def get_font_preamble(cls): @@ -228,8 +234,6 @@ def _get_tex_source(cls, tex, fontsize): r"\begin{document}", r"% The empty hbox ensures that a page is printed even for empty", r"% inputs, except when using psfrag which gets confused by it.", - r"% matplotlibbaselinemarker is used by dviread to detect the", - r"% last line's baseline.", rf"\fontsize{{{fontsize}}}{{{baselineskip}}}%", r"\ifdefined\psfrag\else\hbox{}\fi%", rf"{{{fontcmd} {tex}}}%", @@ -243,17 +247,16 @@ def make_tex(cls, tex, fontsize): Return the file name. """ - texfile = cls.get_basefile(tex, fontsize) + ".tex" - Path(texfile).write_text(cls._get_tex_source(tex, fontsize), - encoding='utf-8') - return texfile + texpath = cls._get_base_path(tex, fontsize).with_suffix(".tex") + texpath.write_text(cls._get_tex_source(tex, fontsize), encoding='utf-8') + return str(texpath) @classmethod def _run_checked_subprocess(cls, command, tex, *, cwd=None): _log.debug(cbook._pformat_subprocess(command)) try: report = subprocess.check_output( - command, cwd=cwd if cwd is not None else cls._texcache, + command, cwd=cwd if cwd is not None else cls._cache_dir, stderr=subprocess.STDOUT) except FileNotFoundError as exc: raise RuntimeError( @@ -281,11 +284,9 @@ def make_dvi(cls, tex, fontsize): Return the file name. """ - basefile = cls.get_basefile(tex, fontsize) - dvifile = '%s.dvi' % basefile - if not os.path.exists(dvifile): - texfile = Path(cls.make_tex(tex, fontsize)) - # Generate the dvi in a temporary directory to avoid race + dvipath = cls._get_base_path(tex, fontsize).with_suffix(".dvi") + if not dvipath.exists(): + # Generate the tex and dvi in a temporary directory to avoid race # conditions e.g. if multiple processes try to process the same tex # string at the same time. Having tmpdir be a subdirectory of the # final output dir ensures that they are on the same filesystem, @@ -294,15 +295,17 @@ def make_dvi(cls, tex, fontsize): # the absolute path may contain characters (e.g. ~) that TeX does # not support; n.b. relative paths cannot traverse parents, or it # will be blocked when `openin_any = p` in texmf.cnf). - cwd = Path(dvifile).parent - with TemporaryDirectory(dir=cwd) as tmpdir: - tmppath = Path(tmpdir) + with TemporaryDirectory(dir=dvipath.parent) as tmpdir: + Path(tmpdir, "file.tex").write_text( + cls._get_tex_source(tex, fontsize), encoding='utf-8') cls._run_checked_subprocess( ["latex", "-interaction=nonstopmode", "--halt-on-error", - f"--output-directory={tmppath.name}", - f"{texfile.name}"], tex, cwd=cwd) - (tmppath / Path(dvifile).name).replace(dvifile) - return dvifile + "file.tex"], tex, cwd=tmpdir) + Path(tmpdir, "file.dvi").replace(dvipath) + # Also move the tex source to the main cache directory, but + # only for backcompat. + Path(tmpdir, "file.tex").replace(dvipath.with_suffix(".tex")) + return str(dvipath) @classmethod def make_png(cls, tex, fontsize, dpi): @@ -311,22 +314,22 @@ def make_png(cls, tex, fontsize, dpi): Return the file name. """ - basefile = cls.get_basefile(tex, fontsize, dpi) - pngfile = '%s.png' % basefile - # see get_rgba for a discussion of the background - if not os.path.exists(pngfile): - dvifile = cls.make_dvi(tex, fontsize) - cmd = ["dvipng", "-bg", "Transparent", "-D", str(dpi), - "-T", "tight", "-o", pngfile, dvifile] - # When testing, disable FreeType rendering for reproducibility; but - # dvipng 1.16 has a bug (fixed in f3ff241) that breaks --freetype0 - # mode, so for it we keep FreeType enabled; the image will be - # slightly off. - if (getattr(mpl, "_called_from_pytest", False) and - mpl._get_executable_info("dvipng").raw_version != "1.16"): - cmd.insert(1, "--freetype0") - cls._run_checked_subprocess(cmd, tex) - return pngfile + pngpath = cls._get_base_path(tex, fontsize, dpi).with_suffix(".png") + if not pngpath.exists(): + dvipath = cls.make_dvi(tex, fontsize) + with TemporaryDirectory(dir=pngpath.parent) as tmpdir: + cmd = ["dvipng", "-bg", "Transparent", "-D", str(dpi), + "-T", "tight", "-o", "file.png", dvipath] + # When testing, disable FreeType rendering for reproducibility; + # but dvipng 1.16 has a bug (fixed in f3ff241) that breaks + # --freetype0 mode, so for it we keep FreeType enabled; the + # image will be slightly off. + if (getattr(mpl, "_called_from_pytest", False) and + mpl._get_executable_info("dvipng").raw_version != "1.16"): + cmd.insert(1, "--freetype0") + cls._run_checked_subprocess(cmd, tex, cwd=tmpdir) + Path(tmpdir, "file.png").replace(pngpath) + return str(pngpath) @classmethod def get_grey(cls, tex, fontsize=None, dpi=None): @@ -337,7 +340,7 @@ def get_grey(cls, tex, fontsize=None, dpi=None): alpha = cls._grey_arrayd.get(key) if alpha is None: pngfile = cls.make_png(tex, fontsize, dpi) - rgba = mpl.image.imread(os.path.join(cls._texcache, pngfile)) + rgba = mpl.image.imread(pngfile) cls._grey_arrayd[key] = alpha = rgba[:, :, -1] return alpha @@ -363,9 +366,9 @@ def get_text_width_height_descent(cls, tex, fontsize, renderer=None): """Return width, height and descent of the text.""" if tex.strip() == '': return 0, 0, 0 - dvifile = cls.make_dvi(tex, fontsize) + dvipath = cls.make_dvi(tex, fontsize) dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1 - with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi: + with dviread.Dvi(dvipath, 72 * dpi_fraction) as dvi: page, = dvi # A total height (including the descent) needs to be returned. return page.width, page.height + page.descent, page.descent diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index acde4fb179a2..0be32ca86009 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -64,17 +64,89 @@ def _get_textbox(text, renderer): def _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi): """Call ``renderer.get_text_width_height_descent``, caching the results.""" - # Cached based on a copy of fontprop so that later in-place mutations of - # the passed-in argument do not mess up the cache. - return _get_text_metrics_with_cache_impl( - weakref.ref(renderer), text, fontprop.copy(), ismath, dpi) + # hit the outer cache layer and get the function to compute the metrics + # for this renderer instance + get_text_metrics = _get_text_metrics_function(renderer) + # call the function to compute the metrics and return + # + # We pass a copy of the fontprop because FontProperties is both mutable and + # has a `__hash__` that depends on that mutable state. This is not ideal + # as it means the hash of an object is not stable over time which leads to + # very confusing behavior when used as keys in dictionaries or hashes. + return get_text_metrics(text, fontprop.copy(), ismath, dpi) -@functools.lru_cache(4096) -def _get_text_metrics_with_cache_impl( - renderer_ref, text, fontprop, ismath, dpi): - # dpi is unused, but participates in cache invalidation (via the renderer). - return renderer_ref().get_text_width_height_descent(text, fontprop, ismath) + +def _get_text_metrics_function(input_renderer, _cache=weakref.WeakKeyDictionary()): + """ + Helper function to provide a two-layered cache for font metrics + + + To get the rendered size of a size of string we need to know: + - what renderer we are using + - the current dpi of the renderer + - the string + - the font properties + - is it math text or not + + We do this as a two-layer cache with the outer layer being tied to a + renderer instance and the inner layer handling everything else. + + The outer layer is implemented as `.WeakKeyDictionary` keyed on the + renderer. As long as someone else is holding a hard ref to the renderer + we will keep the cache alive, but it will be automatically dropped when + the renderer is garbage collected. + + The inner layer is provided by an lru_cache with a large maximum size (such + that we expect very few cache misses in actual use cases). As the + dpi is mutable on the renderer, we need to explicitly include it as part of + the cache key on the inner layer even though we do not directly use it (it is + used in the method call on the renderer). + + This function takes a renderer and returns a function that can be used to + get the font metrics. + + Parameters + ---------- + input_renderer : maplotlib.backend_bases.RendererBase + The renderer to set the cache up for. + + _cache : dict, optional + We are using the mutable default value to attach the cache to the function. + + In principle you could pass a different dict-like to this function to inject + a different cache, but please don't. This is an internal function not meant to + be reused outside of the narrow context we need it for. + + There is a possible race condition here between threads, we may need to drop the + mutable default and switch to a threadlocal variable in the future. + + """ + if (_text_metrics := _cache.get(input_renderer, None)) is None: + # We are going to include this in the closure we put as values in the + # cache. Closing over a hard-ref would create an unbreakable reference + # cycle. + renderer_ref = weakref.ref(input_renderer) + + # define the function locally to get a new lru_cache per renderer + @functools.lru_cache(4096) + # dpi is unused, but participates in cache invalidation (via the renderer). + def _text_metrics(text, fontprop, ismath, dpi): + # this should never happen under normal use, but this is a better error to + # raise than an AttributeError on `None` + if (local_renderer := renderer_ref()) is None: + raise RuntimeError( + "Trying to get text metrics for a renderer that no longer exists. " + "This should never happen and is evidence of a bug elsewhere." + ) + # do the actual method call we need and return the result + return local_renderer.get_text_width_height_descent(text, fontprop, ismath) + + # stash the function for later use. + _cache[input_renderer] = _text_metrics + + # return the inner function + return _text_metrics @_docstring.interpd diff --git a/lib/matplotlib/text.pyi b/lib/matplotlib/text.pyi index 41c7b761ae32..7223693945ec 100644 --- a/lib/matplotlib/text.pyi +++ b/lib/matplotlib/text.pyi @@ -2,7 +2,7 @@ from .artist import Artist from .backend_bases import RendererBase from .font_manager import FontProperties from .offsetbox import DraggableAnnotation -from .path import Path +from pathlib import Path from .patches import FancyArrowPatch, FancyBboxPatch from .textpath import ( # noqa: F401, reexported API TextPath as TextPath, diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index b57597ded363..8deae19c42e7 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -234,7 +234,9 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, # characters into strings. t1_encodings = {} for text in page.text: - font = get_font(text.font_path) + font = get_font(text.font.resolve_path()) + if text.font.subfont: + raise NotImplementedError("Indexing TTC fonts is not supported yet") char_id = self._get_char_id(font, text.glyph) if char_id not in glyph_map: font.clear() diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index b5c12e7f4905..e27d71974471 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -1029,7 +1029,7 @@ def __call__(self, x, pos=None): return '' vmin, vmax = self.axis.get_view_interval() - vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) + vmin, vmax = mtransforms._nonsingular(vmin, vmax, expander=0.05) s = self._num_to_string(x, vmin, vmax) return self.fix_minus(s) @@ -1730,7 +1730,7 @@ def nonsingular(self, v0, v1): default view limits. - Otherwise, ``(v0, v1)`` is returned without modification. """ - return mtransforms.nonsingular(v0, v1, expander=.05) + return mtransforms._nonsingular(v0, v1, expander=.05) def view_limits(self, vmin, vmax): """ @@ -1738,7 +1738,7 @@ def view_limits(self, vmin, vmax): Subclasses should override this method to change locator behaviour. """ - return mtransforms.nonsingular(vmin, vmax) + return mtransforms._nonsingular(vmin, vmax) class IndexLocator(Locator): @@ -1881,7 +1881,7 @@ def __call__(self): return self.tick_values(vmin, vmax) def tick_values(self, vmin, vmax): - vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) + vmin, vmax = mtransforms._nonsingular(vmin, vmax, expander=0.05) if (vmin, vmax) in self.presets: return self.presets[(vmin, vmax)] @@ -1910,7 +1910,7 @@ def view_limits(self, vmin, vmax): vmin = math.floor(scale * vmin) / scale vmax = math.ceil(scale * vmax) / scale - return mtransforms.nonsingular(vmin, vmax) + return mtransforms._nonsingular(vmin, vmax) class MultipleLocator(Locator): @@ -1980,7 +1980,7 @@ def view_limits(self, dmin, dmax): vmin = dmin vmax = dmax - return mtransforms.nonsingular(vmin, vmax) + return mtransforms._nonsingular(vmin, vmax) def scale_range(vmin, vmax, n=1, threshold=100): @@ -2236,7 +2236,7 @@ def tick_values(self, vmin, vmax): if self._symmetric: vmax = max(abs(vmin), abs(vmax)) vmin = -vmax - vmin, vmax = mtransforms.nonsingular( + vmin, vmax = mtransforms._nonsingular( vmin, vmax, expander=1e-13, tiny=1e-14) locs = self._raw_ticks(vmin, vmax) @@ -2254,7 +2254,7 @@ def view_limits(self, dmin, dmax): dmax = max(abs(dmin), abs(dmax)) dmin = -dmax - dmin, dmax = mtransforms.nonsingular( + dmin, dmax = mtransforms._nonsingular( dmin, dmax, expander=1e-12, tiny=1e-13) if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': @@ -2522,10 +2522,12 @@ def tick_values(self, vmin, vmax): if (len(subs) > 1 and stride == 1 - and ((vmin <= ticklocs) & (ticklocs <= vmax)).sum() <= 1): + and (len(decades) - 2 # major + + ((vmin <= ticklocs) & (ticklocs <= vmax)).sum()) # minor + <= 1): # If we're a minor locator *that expects at least two ticks per # decade* and the major locator stride is 1 and there's no more - # than one minor tick, switch to AutoLocator. + # than one major or minor tick, switch to AutoLocator. return AutoLocator().tick_values(vmin, vmax) else: return self.raise_if_exceeds(ticklocs) @@ -2716,7 +2718,7 @@ def view_limits(self, vmin, vmax): vmin = _decade_less(vmin, b) vmax = _decade_greater(vmax, b) - return mtransforms.nonsingular(vmin, vmax) + return mtransforms._nonsingular(vmin, vmax) class AsinhLocator(Locator): diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 350113c56170..44d01926f2e8 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -44,7 +44,7 @@ import numpy as np from numpy.linalg import inv -from matplotlib import _api +from matplotlib import _api, _docstring from matplotlib._path import affine_transform, count_bboxes_overlapping_bbox from .path import Path @@ -377,6 +377,29 @@ def extents(self): def get_points(self): raise NotImplementedError + def _is_finite(self): + """ + Return whether the bounding box is finite and not degenerate to a + single point. + + We count the box as finite if neither width nor height are infinite + and at least one direction is non-zero; i.e. a point is not finite, + but a horizontal or vertical line is. + + .. versionadded:: 3.11 + + Notes + ----- + We keep this private for now because concise naming is hard and + because we are not sure how universal the concept is. It is + currently used only for filtering bboxes to be included in + tightbbox calculation, but I'm unsure whether single points + should be included there as well. + """ + width = self.width + height = self.height + return (width > 0 or height > 0) and width < np.inf and height < np.inf + def containsx(self, x): """ Return whether *x* is in the closed (:attr:`x0`, :attr:`x1`) interval. @@ -1418,7 +1441,7 @@ def contains_branch(self, other): return True return False - def contains_branch_seperately(self, other_transform): + def contains_branch_separately(self, other_transform): """ Return whether the given branch is a sub-tree of this transform on each separate dimension. @@ -1426,16 +1449,21 @@ def contains_branch_seperately(self, other_transform): A common use for this method is to identify if a transform is a blended transform containing an Axes' data transform. e.g.:: - x_isdata, y_isdata = trans.contains_branch_seperately(ax.transData) + x_isdata, y_isdata = trans.contains_branch_separately(ax.transData) """ if self.output_dims != 2: - raise ValueError('contains_branch_seperately only supports ' + raise ValueError('contains_branch_separately only supports ' 'transforms with 2 output dimensions') # for a non-blended transform each separate dimension is the same, so # just return the appropriate shape. return (self.contains_branch(other_transform), ) * 2 + # Permanent alias for backwards compatibility (historical typo) + def contains_branch_seperately(self, other_transform): + """:meta private:""" + return self.contains_branch_separately(other_transform) + def __sub__(self, other): """ Compose *self* with the inverse of *other*, cancelling identical terms @@ -2185,7 +2213,7 @@ def __eq__(self, other): else: return NotImplemented - def contains_branch_seperately(self, transform): + def contains_branch_separately(self, transform): return (self._x.contains_branch(transform), self._y.contains_branch(transform)) @@ -2411,14 +2439,14 @@ def _iter_break_from_left_to_right(self): for left, right in self._b._iter_break_from_left_to_right(): yield self._a + left, right - def contains_branch_seperately(self, other_transform): + def contains_branch_separately(self, other_transform): # docstring inherited if self.output_dims != 2: - raise ValueError('contains_branch_seperately only supports ' + raise ValueError('contains_branch_separately only supports ' 'transforms with 2 output dimensions') if self == other_transform: return (True, True) - return self._b.contains_branch_seperately(other_transform) + return self._b.contains_branch_separately(other_transform) depth = property(lambda self: self._a.depth + self._b.depth) is_affine = property(lambda self: self._a.is_affine and self._b.is_affine) @@ -2837,7 +2865,7 @@ def _revalidate(self): super()._revalidate() -def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True): +def _nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True): """ Modify the endpoints of a range as needed to avoid singularities. @@ -2895,7 +2923,13 @@ def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True): return vmin, vmax -def interval_contains(interval, val): +@_api.deprecated("3.11") +@_docstring.copy(_nonsingular) +def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True): + return _nonsingular(vmin, vmax, expander, tiny, increasing) + + +def _interval_contains(interval, val): """ Check, inclusively, whether an interval includes a given value. @@ -2917,6 +2951,12 @@ def interval_contains(interval, val): return a <= val <= b +@_api.deprecated("3.11") +@_docstring.copy(_interval_contains) +def interval_contains(interval, val): + return _interval_contains(interval, val) + + def _interval_contains_close(interval, val, rtol=1e-10): """ Check, inclusively, whether an interval includes a given value, with the @@ -2946,7 +2986,7 @@ def _interval_contains_close(interval, val, rtol=1e-10): return a - rtol <= val <= b + rtol -def interval_contains_open(interval, val): +def _interval_contains_open(interval, val): """ Check, excluding endpoints, whether an interval includes a given value. @@ -2966,6 +3006,12 @@ def interval_contains_open(interval, val): return a < val < b or a > val > b +@_api.deprecated("3.11") +@_docstring.copy(_interval_contains_open) +def interval_contains_open(interval, val): + return _interval_contains_open(interval, val) + + def offset_copy(trans, fig=None, x=0.0, y=0.0, units='inches'): """ Return a new transform with an added offset. diff --git a/lib/matplotlib/transforms.pyi b/lib/matplotlib/transforms.pyi index 07d299be297c..ebee3954a3a7 100644 --- a/lib/matplotlib/transforms.pyi +++ b/lib/matplotlib/transforms.pyi @@ -65,6 +65,7 @@ class BboxBase(TransformNode): @property def extents(self) -> tuple[float, float, float, float]: ... def get_points(self) -> np.ndarray: ... + def _is_finite(self) -> bool: ... def containsx(self, x: float) -> bool: ... def containsy(self, y: float) -> bool: ... def contains(self, x: float, y: float) -> bool: ... @@ -189,9 +190,10 @@ class Transform(TransformNode): @property def depth(self) -> int: ... def contains_branch(self, other: Transform) -> bool: ... - def contains_branch_seperately( + def contains_branch_separately( self, other_transform: Transform ) -> Sequence[bool]: ... + contains_branch_seperately = contains_branch_separately # Alias (historical typo) def __sub__(self, other: Transform) -> Transform: ... def __array__(self, *args, **kwargs) -> np.ndarray: ... def transform(self, values: ArrayLike) -> np.ndarray: ... @@ -252,7 +254,7 @@ class IdentityTransform(Affine2DBase): ... class _BlendedMixin: def __eq__(self, other: object) -> bool: ... - def contains_branch_seperately(self, transform: Transform) -> Sequence[bool]: ... + def contains_branch_separately(self, transform: Transform) -> Sequence[bool]: ... class BlendedGenericTransform(_BlendedMixin, Transform): input_dims: Literal[2] @@ -314,6 +316,13 @@ class TransformedPath(TransformNode): class TransformedPatchPath(TransformedPath): def __init__(self, patch: Patch) -> None: ... +def _nonsingular( + vmin: float, + vmax: float, + expander: float = ..., + tiny: float = ..., + increasing: bool = ..., +) -> tuple[float, float]: ... def nonsingular( vmin: float, vmax: float, @@ -321,7 +330,9 @@ def nonsingular( tiny: float = ..., increasing: bool = ..., ) -> tuple[float, float]: ... +def _interval_contains(interval: tuple[float, float], val: float) -> bool: ... def interval_contains(interval: tuple[float, float], val: float) -> bool: ... +def _interval_contains_open(interval: tuple[float, float], val: float) -> bool: ... def interval_contains_open(interval: tuple[float, float], val: float) -> bool: ... def offset_copy( trans: Transform, diff --git a/lib/matplotlib/tri/_tripcolor.py b/lib/matplotlib/tri/_tripcolor.py index f3c26b0b25ff..5a5b24522d17 100644 --- a/lib/matplotlib/tri/_tripcolor.py +++ b/lib/matplotlib/tri/_tripcolor.py @@ -163,5 +163,7 @@ def tripcolor(ax, *args, alpha=1.0, norm=None, cmap=None, vmin=None, corners = (minx, miny), (maxx, maxy) ax.update_datalim(corners) ax.autoscale_view() - ax.add_collection(collection) + # TODO: check whether the above explicit limit handling can be + # replaced by autolim=True + ax.add_collection(collection, autolim=False) return collection diff --git a/lib/matplotlib/typing.py b/lib/matplotlib/typing.py index e3719235cdb8..d2e12c6e08d9 100644 --- a/lib/matplotlib/typing.py +++ b/lib/matplotlib/typing.py @@ -12,7 +12,8 @@ """ from collections.abc import Hashable, Sequence import pathlib -from typing import Any, Callable, Literal, TypeAlias, TypeVar, Union +from typing import Any, Literal, TypeAlias, TypeVar, Union +from collections.abc import Callable from . import path from ._enums import JoinStyle, CapStyle @@ -69,7 +70,16 @@ ) """See :doc:`/gallery/lines_bars_and_markers/markevery_demo`.""" -MarkerType: TypeAlias = str | path.Path | MarkerStyle +MarkerType: TypeAlias = ( + path.Path | MarkerStyle | str | # str required for "$...$" marker + Literal[ + ".", ",", "o", "v", "^", "<", ">", + "1", "2", "3", "4", "8", "s", "p", + "P", "*", "h", "H", "+", "x", "X", + "D", "d", "|", "_", "none", " ", + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 + ] | list[tuple[int, int]] | tuple[int, Literal[0, 1, 2], int] +) """ Marker specification. See :doc:`/gallery/lines_bars_and_markers/marker_reference`. """ @@ -83,6 +93,9 @@ CapStyleType: TypeAlias = CapStyle | Literal["butt", "projecting", "round"] """Line cap styles. See :doc:`/gallery/lines_bars_and_markers/capstyle`.""" +LogLevel: TypeAlias = Literal["NOTSET", "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] +"""Literal type for valid logging levels accepted by `set_loglevel()`.""" + CoordsBaseType = Union[ str, Artist, @@ -137,3 +150,425 @@ ResizeEventType, CloseEventType, ] + +LegendLocType: TypeAlias = ( + Literal[ + # for simplicity, we don't distinguish the between allowed positions for + # Axes legend and figure legend. It's still better to limit the allowed + # range to the union of both rather than to accept arbitrary strings + "upper right", "upper left", "lower left", "lower right", + "right", "center left", "center right", "lower center", "upper center", + "center", + # Axes only + "best", + # Figure only + "outside upper left", "outside upper center", "outside upper right", + "outside right upper", "outside right center", "outside right lower", + "outside lower right", "outside lower center", "outside lower left", + "outside left lower", "outside left center", "outside left upper", + ] | + tuple[float, float] | + int +) + +RcKeyType: TypeAlias = Literal[ + "agg.path.chunksize", + "animation.bitrate", + "animation.codec", + "animation.convert_args", + "animation.convert_path", + "animation.embed_limit", + "animation.ffmpeg_args", + "animation.ffmpeg_path", + "animation.frame_format", + "animation.html", + "animation.writer", + "axes.autolimit_mode", + "axes.axisbelow", + "axes.edgecolor", + "axes.facecolor", + "axes.formatter.limits", + "axes.formatter.min_exponent", + "axes.formatter.offset_threshold", + "axes.formatter.use_locale", + "axes.formatter.use_mathtext", + "axes.formatter.useoffset", + "axes.grid", + "axes.grid.axis", + "axes.grid.which", + "axes.labelcolor", + "axes.labelpad", + "axes.labelsize", + "axes.labelweight", + "axes.linewidth", + "axes.prop_cycle", + "axes.spines.bottom", + "axes.spines.left", + "axes.spines.right", + "axes.spines.top", + "axes.titlecolor", + "axes.titlelocation", + "axes.titlepad", + "axes.titlesize", + "axes.titleweight", + "axes.titley", + "axes.unicode_minus", + "axes.xmargin", + "axes.ymargin", + "axes.zmargin", + "axes3d.automargin", + "axes3d.depthshade", + "axes3d.depthshade_minalpha", + "axes3d.grid", + "axes3d.mouserotationstyle", + "axes3d.trackballborder", + "axes3d.trackballsize", + "axes3d.xaxis.panecolor", + "axes3d.yaxis.panecolor", + "axes3d.zaxis.panecolor", + "backend", + "backend_fallback", + "boxplot.bootstrap", + "boxplot.boxprops.color", + "boxplot.boxprops.linestyle", + "boxplot.boxprops.linewidth", + "boxplot.capprops.color", + "boxplot.capprops.linestyle", + "boxplot.capprops.linewidth", + "boxplot.flierprops.color", + "boxplot.flierprops.linestyle", + "boxplot.flierprops.linewidth", + "boxplot.flierprops.marker", + "boxplot.flierprops.markeredgecolor", + "boxplot.flierprops.markeredgewidth", + "boxplot.flierprops.markerfacecolor", + "boxplot.flierprops.markersize", + "boxplot.meanline", + "boxplot.meanprops.color", + "boxplot.meanprops.linestyle", + "boxplot.meanprops.linewidth", + "boxplot.meanprops.marker", + "boxplot.meanprops.markeredgecolor", + "boxplot.meanprops.markerfacecolor", + "boxplot.meanprops.markersize", + "boxplot.medianprops.color", + "boxplot.medianprops.linestyle", + "boxplot.medianprops.linewidth", + "boxplot.notch", + "boxplot.patchartist", + "boxplot.showbox", + "boxplot.showcaps", + "boxplot.showfliers", + "boxplot.showmeans", + "boxplot.vertical", + "boxplot.whiskerprops.color", + "boxplot.whiskerprops.linestyle", + "boxplot.whiskerprops.linewidth", + "boxplot.whiskers", + "contour.algorithm", + "contour.corner_mask", + "contour.linewidth", + "contour.negative_linestyle", + "date.autoformatter.day", + "date.autoformatter.hour", + "date.autoformatter.microsecond", + "date.autoformatter.minute", + "date.autoformatter.month", + "date.autoformatter.second", + "date.autoformatter.year", + "date.converter", + "date.epoch", + "date.interval_multiples", + "docstring.hardcopy", + "errorbar.capsize", + "figure.autolayout", + "figure.constrained_layout.h_pad", + "figure.constrained_layout.hspace", + "figure.constrained_layout.use", + "figure.constrained_layout.w_pad", + "figure.constrained_layout.wspace", + "figure.dpi", + "figure.edgecolor", + "figure.facecolor", + "figure.figsize", + "figure.frameon", + "figure.hooks", + "figure.labelsize", + "figure.labelweight", + "figure.max_open_warning", + "figure.raise_window", + "figure.subplot.bottom", + "figure.subplot.hspace", + "figure.subplot.left", + "figure.subplot.right", + "figure.subplot.top", + "figure.subplot.wspace", + "figure.titlesize", + "figure.titleweight", + "font.cursive", + "font.enable_last_resort", + "font.family", + "font.fantasy", + "font.monospace", + "font.sans-serif", + "font.serif", + "font.size", + "font.stretch", + "font.style", + "font.variant", + "font.weight", + "grid.alpha", + "grid.color", + "grid.linestyle", + "grid.linewidth", + "grid.major.alpha", + "grid.major.color", + "grid.major.linestyle", + "grid.major.linewidth", + "grid.minor.alpha", + "grid.minor.color", + "grid.minor.linestyle", + "grid.minor.linewidth", + "hatch.color", + "hatch.linewidth", + "hist.bins", + "image.aspect", + "image.cmap", + "image.composite_image", + "image.interpolation", + "image.interpolation_stage", + "image.lut", + "image.origin", + "image.resample", + "interactive", + "keymap.back", + "keymap.copy", + "keymap.forward", + "keymap.fullscreen", + "keymap.grid", + "keymap.grid_minor", + "keymap.help", + "keymap.home", + "keymap.pan", + "keymap.quit", + "keymap.quit_all", + "keymap.save", + "keymap.xscale", + "keymap.yscale", + "keymap.zoom", + "legend.borderaxespad", + "legend.borderpad", + "legend.columnspacing", + "legend.edgecolor", + "legend.facecolor", + "legend.fancybox", + "legend.fontsize", + "legend.framealpha", + "legend.frameon", + "legend.handleheight", + "legend.handlelength", + "legend.handletextpad", + "legend.labelcolor", + "legend.labelspacing", + "legend.linewidth", + "legend.loc", + "legend.markerscale", + "legend.numpoints", + "legend.scatterpoints", + "legend.shadow", + "legend.title_fontsize", + "lines.antialiased", + "lines.color", + "lines.dash_capstyle", + "lines.dash_joinstyle", + "lines.dashdot_pattern", + "lines.dashed_pattern", + "lines.dotted_pattern", + "lines.linestyle", + "lines.linewidth", + "lines.marker", + "lines.markeredgecolor", + "lines.markeredgewidth", + "lines.markerfacecolor", + "lines.markersize", + "lines.scale_dashes", + "lines.solid_capstyle", + "lines.solid_joinstyle", + "macosx.window_mode", + "markers.fillstyle", + "mathtext.bf", + "mathtext.bfit", + "mathtext.cal", + "mathtext.default", + "mathtext.fallback", + "mathtext.fontset", + "mathtext.it", + "mathtext.rm", + "mathtext.sf", + "mathtext.tt", + "patch.antialiased", + "patch.edgecolor", + "patch.facecolor", + "patch.force_edgecolor", + "patch.linewidth", + "path.effects", + "path.simplify", + "path.simplify_threshold", + "path.sketch", + "path.snap", + "pcolor.shading", + "pcolormesh.snap", + "pdf.compression", + "pdf.fonttype", + "pdf.inheritcolor", + "pdf.use14corefonts", + "pgf.preamble", + "pgf.rcfonts", + "pgf.texsystem", + "polaraxes.grid", + "ps.distiller.res", + "ps.fonttype", + "ps.papersize", + "ps.useafm", + "ps.usedistiller", + "savefig.bbox", + "savefig.directory", + "savefig.dpi", + "savefig.edgecolor", + "savefig.facecolor", + "savefig.format", + "savefig.orientation", + "savefig.pad_inches", + "savefig.transparent", + "scatter.edgecolors", + "scatter.marker", + "svg.fonttype", + "svg.hashsalt", + "svg.id", + "svg.image_inline", + "text.antialiased", + "text.color", + "text.hinting", + "text.hinting_factor", + "text.kerning_factor", + "text.latex.preamble", + "text.parse_math", + "text.usetex", + "timezone", + "tk.window_focus", + "toolbar", + "webagg.address", + "webagg.open_in_browser", + "webagg.port", + "webagg.port_retries", + "xaxis.labellocation", + "xtick.alignment", + "xtick.bottom", + "xtick.color", + "xtick.direction", + "xtick.labelbottom", + "xtick.labelcolor", + "xtick.labelsize", + "xtick.labeltop", + "xtick.major.bottom", + "xtick.major.pad", + "xtick.major.size", + "xtick.major.top", + "xtick.major.width", + "xtick.minor.bottom", + "xtick.minor.ndivs", + "xtick.minor.pad", + "xtick.minor.size", + "xtick.minor.top", + "xtick.minor.visible", + "xtick.minor.width", + "xtick.top", + "yaxis.labellocation", + "ytick.alignment", + "ytick.color", + "ytick.direction", + "ytick.labelcolor", + "ytick.labelleft", + "ytick.labelright", + "ytick.labelsize", + "ytick.left", + "ytick.major.left", + "ytick.major.pad", + "ytick.major.right", + "ytick.major.size", + "ytick.major.width", + "ytick.minor.left", + "ytick.minor.ndivs", + "ytick.minor.pad", + "ytick.minor.right", + "ytick.minor.size", + "ytick.minor.visible", + "ytick.minor.width", + "ytick.right", +] + +RcGroupKeyType: TypeAlias = Literal[ + "agg", + "agg.path", + "animation", + "axes", + "axes.formatter", + "axes.grid", + "axes.spines", + "axes3d", + "axes3d.xaxis", + "axes3d.yaxis", + "axes3d.zaxis", + "boxplot", + "boxplot.boxprops", + "boxplot.capprops", + "boxplot.flierprops", + "boxplot.meanprops", + "boxplot.medianprops", + "boxplot.whiskerprops", + "contour", + "date", + "date.autoformatter", + "docstring", + "errorbar", + "figure", + "figure.constrained_layout", + "figure.subplot", + "font", + "grid", + "grid.major", + "grid.minor", + "hatch", + "hist", + "image", + "keymap", + "legend", + "lines", + "macosx", + "markers", + "mathtext", + "patch", + "path", + "pcolor", + "pcolormesh", + "pdf", + "pgf", + "polaraxes", + "ps", + "ps.distiller", + "savefig", + "scatter", + "svg", + "text", + "text.latex", + "tk", + "webagg", + "xaxis", + "xtick", + "xtick.major", + "xtick.minor", + "yaxis", + "ytick", + "ytick.major", + "ytick.minor", +] diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 9ded7c61ce2d..cfe8cdc7d9f5 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -1,8 +1,6 @@ """ -GUI neutral widgets -=================== - Widgets that are designed to work for any of the GUI backends. + All of these widgets require you to predefine an `~.axes.Axes` instance and pass that as the first parameter. Matplotlib doesn't try to be too smart with respect to layout -- you will have to figure out how @@ -11,6 +9,8 @@ from contextlib import ExitStack import copy +import enum +import functools import itertools from numbers import Integral, Number @@ -116,8 +116,15 @@ class AxesWidget(Widget): def __init__(self, ax): self.ax = ax self._cids = [] + self._blit_background_id = None + + def __del__(self): + if self._blit_background_id is not None: + self.canvas._release_blit_background_id(self._blit_background_id) - canvas = property(lambda self: self.ax.get_figure(root=True).canvas) + canvas = property( + lambda self: getattr(self.ax.get_figure(root=True), 'canvas', None) + ) def connect_event(self, event, callback): """ @@ -134,15 +141,59 @@ def disconnect_events(self): for c in self._cids: self.canvas.mpl_disconnect(c) - def _get_data_coords(self, event): - """Return *event*'s data coordinates in this widget's Axes.""" - # This method handles the possibility that event.inaxes != self.ax (which may - # occur if multiple Axes are overlaid), in which case event.xdata/.ydata will - # be wrong. Note that we still special-case the common case where - # event.inaxes == self.ax and avoid re-running the inverse data transform, - # because that can introduce floating point errors for synthetic events. - return ((event.xdata, event.ydata) if event.inaxes is self.ax - else self.ax.transData.inverted().transform((event.x, event.y))) + def ignore(self, event): + # docstring inherited + return super().ignore(event) or self.canvas is None + + def _set_cursor(self, cursor): + """Update the canvas cursor.""" + self.ax.get_figure(root=True).canvas.set_cursor(cursor) + + def _save_blit_background(self, background): + """ + Save a blit background. + + The background is stored on the canvas in a uniquely identifiable way. + It should be read back via `._load_blit_background`. Be prepared that + some events may invalidate the background, in which case + `._load_blit_background` will return None. + + This currently allows at most one background per widget, which is + good enough for all existing widgets. + """ + if self._blit_background_id is None: + self._blit_background_id = self.canvas._get_blit_background_id() + self.canvas._blit_backgrounds[self._blit_background_id] = background + + def _load_blit_background(self): + """Load a blit background; may be None at any time.""" + return self.canvas._blit_backgrounds.get(self._blit_background_id) + + +def _call_with_reparented_event(func): + """ + Event callback decorator ensuring that the callback is called with an event + that has been reparented to the widget's axes. + """ + # This decorator handles the possibility that event.inaxes != self.ax + # (e.g. if multiple Axes are overlaid), in which case event.xdata/.ydata + # will be wrong. Note that we still special-case the common case where + # event.inaxes == self.ax and avoid re-running the inverse data transform, + # because that can introduce floating point errors for synthetic events. + @functools.wraps(func) + def wrapper(self, event): + if event.inaxes is not self.ax: + event = copy.copy(event) + event.guiEvent = None + event.inaxes = self.ax + try: + event.xdata, event.ydata = ( + self.ax.transData.inverted().transform((event.x, event.y))) + except ValueError: # cf LocationEvent._set_inaxes. + event.xdata = event.ydata = None + return func(self, event) + + return wrapper class Button(AxesWidget): @@ -195,7 +246,7 @@ def __init__(self, ax, label, image=None, horizontalalignment='center', transform=ax.transAxes) - self._useblit = useblit and self.canvas.supports_blit + self._useblit = useblit self._observers = cbook.CallbackRegistry(signals=["clicked"]) @@ -209,12 +260,14 @@ def __init__(self, ax, label, image=None, self.color = color self.hovercolor = hovercolor + @_call_with_reparented_event def _click(self, event): if not self.eventson or self.ignore(event) or not self.ax.contains(event)[0]: return if event.canvas.mouse_grabber != self.ax: event.canvas.grab_mouse(self.ax) + @_call_with_reparented_event def _release(self, event): if self.ignore(event) or event.canvas.mouse_grabber != self.ax: return @@ -222,6 +275,7 @@ def _release(self, event): if self.eventson and self.ax.contains(event)[0]: self._observers.process('clicked', event) + @_call_with_reparented_event def _motion(self, event): if self.ignore(event): return @@ -229,7 +283,7 @@ def _motion(self, event): if not colors.same_color(c, self.ax.get_facecolor()): self.ax.set_facecolor(c) if self.drawon: - if self._useblit: + if self._useblit and self.canvas.supports_blit: self.ax.draw_artist(self.ax) self.canvas.blit(self.ax.bbox) else: @@ -364,8 +418,9 @@ def __init__(self, ax, label, valmin, valmax, *, valinit=0.5, valfmt=None, The slider initial position. valfmt : str, default: None - %-format string used to format the slider value. If None, a - `.ScalarFormatter` is used instead. + The way to format the slider value. If a string, it must be in %-format. + If a callable, it must have the signature ``valfmt(val: float) -> str``. + If None, a `.ScalarFormatter` is used. closedmin : bool, default: True Whether the slider interval is closed on the bottom. @@ -520,6 +575,7 @@ def _value_in_bounds(self, val): val = self.slidermax.val return val + @_call_with_reparented_event def _update(self, event): """Update the slider position.""" if self.ignore(event) or event.button != 1: @@ -538,16 +594,18 @@ def _update(self, event): event.canvas.release_mouse(self.ax) return - xdata, ydata = self._get_data_coords(event) val = self._value_in_bounds( - xdata if self.orientation == 'horizontal' else ydata) + event.xdata if self.orientation == 'horizontal' else event.ydata) if val not in [None, self.val]: self.set_val(val) def _format(self, val): """Pretty-print *val*.""" if self.valfmt is not None: - return self.valfmt % val + if callable(self.valfmt): + return self.valfmt(val) + else: + return self.valfmt % val else: _, s, _ = self._fmt.format_ticks([self.valmin, val, self.valmax]) # fmt.get_offset is actually the multiplicative factor, if any. @@ -644,9 +702,11 @@ def __init__( The initial positions of the slider. If None the initial positions will be at the 25th and 75th percentiles of the range. - valfmt : str, default: None - %-format string used to format the slider values. If None, a - `.ScalarFormatter` is used instead. + valfmt : str or callable, default: None + The way to format the range's minimal and maximal values. If a + string, it must be in %-format. If a callable, it must have the + signature ``valfmt(val: float) -> str``. If None, a + `.ScalarFormatter` is used. closedmin : bool, default: True Whether the slider interval is closed on the bottom. @@ -853,6 +913,7 @@ def _update_val_from_pos(self, pos): else: self._active_handle.set_xdata([val]) + @_call_with_reparented_event def _update(self, event): """Update the slider position.""" if self.ignore(event) or event.button != 1: @@ -873,11 +934,10 @@ def _update(self, event): return # determine which handle was grabbed - xdata, ydata = self._get_data_coords(event) handle_index = np.argmin(np.abs( - [h.get_xdata()[0] - xdata for h in self._handles] + [h.get_xdata()[0] - event.xdata for h in self._handles] if self.orientation == "horizontal" else - [h.get_ydata()[0] - ydata for h in self._handles])) + [h.get_ydata()[0] - event.ydata for h in self._handles])) handle = self._handles[handle_index] # these checks ensure smooth behavior if the handles swap which one @@ -885,12 +945,16 @@ def _update(self, event): if handle is not self._active_handle: self._active_handle = handle - self._update_val_from_pos(xdata if self.orientation == "horizontal" else ydata) + self._update_val_from_pos( + event.xdata if self.orientation == "horizontal" else event.ydata) def _format(self, val): """Pretty-print *val*.""" if self.valfmt is not None: - return f"({self.valfmt % val[0]}, {self.valfmt % val[1]})" + if callable(self.valfmt): + return f"({self.valfmt(val[0])}, {self.valfmt(val[1])})" + else: + return f"({self.valfmt % val[0]}, {self.valfmt % val[1]})" else: _, s1, s2, _ = self._fmt.format_ticks( [self.valmin, *val, self.valmax] @@ -1010,8 +1074,11 @@ def __init__(self, ax, labels, actives=None, *, useblit=True, .. versionadded:: 3.7 - label_props : dict, optional - Dictionary of `.Text` properties to be used for the labels. + label_props : dict of lists, optional + Dictionary of `.Text` properties to be used for the labels. Each + dictionary value should be a list of at least a single element. If + the list is of length M, its values are cycled such that the Nth + label gets the (N mod M) property. .. versionadded:: 3.7 frame_props : dict, optional @@ -1039,8 +1106,7 @@ def __init__(self, ax, labels, actives=None, *, useblit=True, if actives is None: actives = [False] * len(labels) - self._useblit = useblit and self.canvas.supports_blit - self._background = None + self._useblit = useblit ys = np.linspace(1, 0, len(labels)+2)[1:-1] @@ -1068,7 +1134,10 @@ def __init__(self, ax, labels, actives=None, *, useblit=True, **cbook.normalize_kwargs(check_props, collections.PathCollection), 'marker': 'x', 'transform': ax.transAxes, - 'animated': self._useblit, + 'animated': self._useblit and self.canvas.supports_blit, + # TODO: This may need an update when switching out the canvas. + # Can set this to `_useblit` only and live with the animated=True + # overhead on unsupported backends. } check_props.setdefault('facecolor', check_props.pop('color', 'black')) self._checks = ax.scatter([0.15] * len(ys), ys, **check_props) @@ -1087,9 +1156,11 @@ def _clear(self, event): """Internal event handler to clear the buttons.""" if self.ignore(event) or self.canvas.is_saving(): return - self._background = self.canvas.copy_from_bbox(self.ax.bbox) + if self._useblit and self.canvas.supports_blit: + self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox)) self.ax.draw_artist(self._checks) + @_call_with_reparented_event def _clicked(self, event): if self.ignore(event) or event.button != 1 or not self.ax.contains(event)[0]: return @@ -1111,7 +1182,8 @@ def set_label_props(self, props): Parameters ---------- props : dict - Dictionary of `.Text` properties to be used for the labels. + Dictionary of `.Text` properties to be used for the labels. Same + format as label_props argument of :class:`CheckButtons`. """ _api.check_isinstance(dict, props=props) props = _expand_text_props(props) @@ -1190,9 +1262,10 @@ def set_active(self, index, state=None): self._checks.set_facecolor(facecolors) if self.drawon: - if self._useblit: - if self._background is not None: - self.canvas.restore_region(self._background) + if self._useblit and self.canvas.supports_blit: + background = self._load_blit_background() + if background is not None: + self.canvas.restore_region(background) self.ax.draw_artist(self._checks) self.canvas.blit(self.ax.bbox) else: @@ -1396,6 +1469,7 @@ def _rendercursor(self): fig.canvas.draw() + @_call_with_reparented_event def _release(self, event): if self.ignore(event): return @@ -1403,6 +1477,7 @@ def _release(self, event): return event.canvas.release_mouse(self.ax) + @_call_with_reparented_event def _keypress(self, event): if self.ignore(event): return @@ -1485,6 +1560,7 @@ def stop_typing(self): # call it once we've already done our cleanup. self._observers.process('submit', self.text) + @_call_with_reparented_event def _click(self, event): if self.ignore(event): return @@ -1500,9 +1576,11 @@ def _click(self, event): self.cursor_index = self.text_disp._char_index_at(event.x) self._rendercursor() + @_call_with_reparented_event def _resize(self, event): self.stop_typing() + @_call_with_reparented_event def _motion(self, event): if self.ignore(event): return @@ -1579,8 +1657,11 @@ def __init__(self, ax, labels, active=0, activecolor=None, *, .. versionadded:: 3.7 - label_props : dict or list of dict, optional - Dictionary of `.Text` properties to be used for the labels. + label_props : dict of lists, optional + Dictionary of `.Text` properties to be used for the labels. Each + dictionary value should be a list of at least a single element. If + the list is of length M, its values are cycled such that the Nth + label gets the (N mod M) property. .. versionadded:: 3.7 radio_props : dict, optional @@ -1622,8 +1703,7 @@ def __init__(self, ax, labels, active=0, activecolor=None, *, ys = np.linspace(1, 0, len(labels) + 2)[1:-1] - self._useblit = useblit and self.canvas.supports_blit - self._background = None + self._useblit = useblit label_props = _expand_text_props(label_props) self.labels = [ @@ -1638,7 +1718,11 @@ def __init__(self, ax, labels, active=0, activecolor=None, *, **radio_props, 'marker': 'o', 'transform': ax.transAxes, - 'animated': self._useblit, + 'animated': self._useblit and self.canvas.supports_blit, + # TODO: This may need an update when switching out the canvas. + # Can set this to `_useblit` only and live with the animated=True + # overhead on unsupported backends. + } radio_props.setdefault('edgecolor', radio_props.get('color', 'black')) radio_props.setdefault('facecolor', @@ -1665,9 +1749,11 @@ def _clear(self, event): """Internal event handler to clear the buttons.""" if self.ignore(event) or self.canvas.is_saving(): return - self._background = self.canvas.copy_from_bbox(self.ax.bbox) + if self._useblit and self.canvas.supports_blit: + self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox)) self.ax.draw_artist(self._buttons) + @_call_with_reparented_event def _clicked(self, event): if self.ignore(event) or event.button != 1 or not self.ax.contains(event)[0]: return @@ -1689,7 +1775,8 @@ def set_label_props(self, props): Parameters ---------- props : dict - Dictionary of `.Text` properties to be used for the labels. + Dictionary of `.Text` properties to be used for the labels. Same + format as label_props argument of :class:`RadioButtons`. """ _api.check_isinstance(dict, props=props) props = _expand_text_props(props) @@ -1756,9 +1843,10 @@ def set_active(self, index): self._buttons.set_facecolor(button_facecolors) if self.drawon: - if self._useblit: - if self._background is not None: - self.canvas.restore_region(self._background) + if self._useblit and self.canvas.supports_blit: + background = self._load_blit_background() + if background is not None: + self.canvas.restore_region(background) self.ax.draw_artist(self._buttons) self.canvas.blit(self.ax.bbox) else: @@ -1907,14 +1995,13 @@ def __init__(self, ax, *, horizOn=True, vertOn=True, useblit=False, self.visible = True self.horizOn = horizOn self.vertOn = vertOn - self.useblit = useblit and self.canvas.supports_blit + self.useblit = useblit and self.canvas.supports_blit # TODO: make dynamic if self.useblit: lineprops['animated'] = True self.lineh = ax.axhline(ax.get_ybound()[0], visible=False, **lineprops) self.linev = ax.axvline(ax.get_xbound()[0], visible=False, **lineprops) - self.background = None self.needclear = False def clear(self, event): @@ -1922,8 +2009,9 @@ def clear(self, event): if self.ignore(event) or self.canvas.is_saving(): return if self.useblit: - self.background = self.canvas.copy_from_bbox(self.ax.bbox) + self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox)) + @_call_with_reparented_event def onmove(self, event): """Internal event handler to draw the cursor when the mouse moves.""" if self.ignore(event): @@ -1938,17 +2026,17 @@ def onmove(self, event): self.needclear = False return self.needclear = True - xdata, ydata = self._get_data_coords(event) - self.linev.set_xdata((xdata, xdata)) + self.linev.set_xdata((event.xdata, event.xdata)) self.linev.set_visible(self.visible and self.vertOn) - self.lineh.set_ydata((ydata, ydata)) + self.lineh.set_ydata((event.ydata, event.ydata)) self.lineh.set_visible(self.visible and self.horizOn) if not (self.visible and (self.vertOn or self.horizOn)): return # Redraw. if self.useblit: - if self.background is not None: - self.canvas.restore_region(self.background) + background = self._load_blit_background() + if background is not None: + self.canvas.restore_region(background) self.ax.draw_artist(self.linev) self.ax.draw_artist(self.lineh) self.canvas.blit(self.ax.bbox) @@ -1961,12 +2049,19 @@ class MultiCursor(Widget): Provide a vertical (default) and/or horizontal line cursor shared between multiple Axes. + Call signatures:: + + MultiCursor(axes, *, ...) + MultiCursor(canvas, axes, *, ...) # deprecated + For the cursor to remain responsive you must keep a reference to it. Parameters ---------- canvas : object - This parameter is entirely unused and only kept for back-compatibility. + This parameter is entirely unused. + + .. deprecated:: 3.11 axes : list of `~matplotlib.axes.Axes` The `~.axes.Axes` to attach the cursor to. @@ -1993,11 +2088,25 @@ class MultiCursor(Widget): See :doc:`/gallery/widgets/multicursor`. """ - def __init__(self, canvas, axes, *, useblit=True, horizOn=False, vertOn=True, + def __init__(self, *args, useblit=True, horizOn=False, vertOn=True, **lineprops): - # canvas is stored only to provide the deprecated .canvas attribute; - # once it goes away the unused argument won't need to be stored at all. - self._canvas = canvas + # Deprecation of canvas as the first attribute. When the deprecation expires: + # - change the signature to __init__(self, axes, *, ...) + # - delete the "Call signatures" block in the docstring + # - delete this block + kwargs = {k: lineprops.pop(k) + for k in list(lineprops) if k in ("canvas", "axes")} + params = _api.select_matching_signature( + [lambda axes: locals(), lambda canvas, axes: locals()], *args, **kwargs) + if "canvas" in params: + _api.warn_deprecated( + "3.11", + message="The canvas parameter in MultiCursor is unused and deprecated " + "since %(since)s. Please remove it and call MultiCursor(axes) " + "instead of MultiCursor(canvas, axes). The latter will start raising " + "an error in %(removal)s" + ) + axes = params["axes"] self.axes = axes self.horizOn = horizOn @@ -2016,6 +2125,7 @@ def __init__(self, canvas, axes, *, useblit=True, horizOn=False, vertOn=True, self.useblit = ( useblit and all(canvas.supports_blit for canvas in self._canvas_infos)) + # TODO: make dynamic if self.useblit: lineprops['animated'] = True @@ -2090,6 +2200,16 @@ def onmove(self, event): class _SelectorWidget(AxesWidget): + """ + The base class for selector widgets. + + This class provides common functionality for selector widgets, + such as handling mouse and keyboard events, managing state modifier keys, etc. + + The class itself is private and may be changed or removed without prior warning. + However, the public API it provides to subclasses is stable and considered + public on the subclasses. + """ def __init__(self, ax, onselect=None, useblit=False, button=None, state_modifier_keys=None, use_data_coordinates=False): @@ -2100,7 +2220,7 @@ def __init__(self, ax, onselect=None, useblit=False, button=None, self.onselect = lambda *args: None else: self.onselect = onselect - self.useblit = useblit and self.canvas.supports_blit + self._useblit = useblit self.connect_default_events() self._state_modifier_keys = dict(move=' ', clear='escape', @@ -2109,8 +2229,6 @@ def __init__(self, ax, onselect=None, useblit=False, button=None, self._state_modifier_keys.update(state_modifier_keys or {}) self._use_data_coordinates = use_data_coordinates - self.background = None - if isinstance(button, Integral): self.validButtons = [button] else: @@ -2126,6 +2244,11 @@ def __init__(self, ax, onselect=None, useblit=False, button=None, self._prev_event = None self._state = set() + @property + def useblit(self): + """Return whether blitting is used (requested and supported by canvas).""" + return self._useblit and self.canvas.supports_blit + def set_active(self, active): super().set_active(active) if active: @@ -2149,6 +2272,8 @@ def update_background(self, event): # `release` can call a draw event even when `ignore` is True. if not self.useblit: return + if self.canvas.is_saving(): + return # saving does not use blitting # Make sure that widget artists don't get accidentally included in the # background, by re-rendering the background if needed (and then # re-re-rendering the canvas with the visible widget artists). @@ -2164,7 +2289,7 @@ def update_background(self, event): for artist in artists: stack.enter_context(artist._cm_set(visible=False)) self.canvas.draw() - self.background = self.canvas.copy_from_bbox(self.ax.bbox) + self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox)) if needs_redraw: for artist in artists: self.ax.draw_artist(artist) @@ -2181,7 +2306,9 @@ def connect_default_events(self): def ignore(self, event): # docstring inherited - if not self.active or not self.ax.get_visible(): + if super().ignore(event): + return True + if not self.ax.get_visible(): return True # If canvas was locked if not self.canvas.widgetlock.available(self): @@ -2209,8 +2336,9 @@ def update(self): self.ax.get_figure(root=True)._get_renderer() is None): return if self.useblit: - if self.background is not None: - self.canvas.restore_region(self.background) + background = self._load_blit_background() + if background is not None: + self.canvas.restore_region(background) else: self.update_background(None) # We need to draw all artists, which are not included in the @@ -2228,9 +2356,8 @@ def _get_data(self, event): """Get the xdata and ydata for event, with limits.""" if event.xdata is None: return None, None - xdata, ydata = self._get_data_coords(event) - xdata = np.clip(xdata, *self.ax.get_xbound()) - ydata = np.clip(ydata, *self.ax.get_ybound()) + xdata = np.clip(event.xdata, *self.ax.get_xbound()) + ydata = np.clip(event.ydata, *self.ax.get_ybound()) return xdata, ydata def _clean_event(self, event): @@ -2250,6 +2377,7 @@ def _clean_event(self, event): self._prev_event = event return event + @_call_with_reparented_event def press(self, event): """Button press handler and validator.""" if not self.ignore(event): @@ -2268,6 +2396,7 @@ def press(self, event): def _press(self, event): """Button press event handler.""" + @_call_with_reparented_event def release(self, event): """Button release event handler and validator.""" if not self.ignore(event) and self._eventpress: @@ -2283,6 +2412,7 @@ def release(self, event): def _release(self, event): """Button release event handler.""" + @_call_with_reparented_event def onmove(self, event): """Cursor move event handler and validator.""" if not self.ignore(event) and self._eventpress: @@ -2294,6 +2424,7 @@ def onmove(self, event): def _onmove(self, event): """Cursor move event handler.""" + @_call_with_reparented_event def on_scroll(self, event): """Mouse scroll event handler and validator.""" if not self.ignore(event): @@ -2302,6 +2433,7 @@ def on_scroll(self, event): def _on_scroll(self, event): """Mouse scroll event handler.""" + @_call_with_reparented_event def on_key_press(self, event): """Key press event handler and validator for all selection widgets.""" if self.active: @@ -2326,6 +2458,7 @@ def on_key_press(self, event): def _on_key_press(self, event): """Key press event handler - for widget-specific key press actions.""" + @_call_with_reparented_event def on_key_release(self, event): """Key release event handler and validator.""" if self.active: @@ -2381,7 +2514,7 @@ def set_props(self, **props): def set_handle_props(self, **handle_props): """ Set the properties of the handles selector artist. See the - `handle_props` argument in the selector docstring to know which + *handle_props* argument in the selector docstring to know which properties are supported. """ if not hasattr(self, '_handles_artists'): @@ -2405,13 +2538,15 @@ def _validate_state(self, state): def add_state(self, state): """ Add a state to define the widget's behavior. See the - `state_modifier_keys` parameters for details. + *state_modifier_keys* parameter in the constructor of the concrete + selector class for details. Parameters ---------- state : str Must be a supported state of the selector. See the - `state_modifier_keys` parameters for details. + *state_modifier_keys* parameter in the constructor of the concrete + selector class for details. Raises ------ @@ -2425,13 +2560,15 @@ def add_state(self, state): def remove_state(self, state): """ Remove a state to define the widget's behavior. See the - `state_modifier_keys` parameters for details. + *state_modifier_keys* parameter in the constructor of the concrete + selector class for details. Parameters ---------- state : str Must be a supported state of the selector. See the - `state_modifier_keys` parameters for details. + *state_modifier_keys* parameter in the constructor of the concrete + selector class for details. Raises ------ @@ -2543,7 +2680,14 @@ def __init__(self, ax, onselect, direction, *, minspan=0, useblit=False, if props is None: props = dict(facecolor='red', alpha=0.5) - props['animated'] = self.useblit + # Note: We set this based on the user setting during ínitialization, + # not on the actual capability of blitting. But the value is + # irrelevant if the backend does not support blitting, so that + # we don't have to dynamically update this on the backend. + # This relies on the current behavior that the request for + # useblit is fixed during initialization and cannot be changed + # afterwards. + props['animated'] = self._useblit self.direction = direction self._extents_on_press = None @@ -2609,7 +2753,7 @@ def _setup_edge_handles(self, props): self._edge_handles = ToolLineHandles(self.ax, positions, direction=self.direction, line_props=props, - useblit=self.useblit) + useblit=self._useblit) @property def _handles_artists(self): @@ -2618,7 +2762,7 @@ def _handles_artists(self): else: return () - def _set_cursor(self, enabled): + def _set_span_cursor(self, *, enabled): """Update the canvas cursor based on direction of the selector.""" if enabled: cursor = (backend_tools.Cursors.RESIZE_HORIZONTAL @@ -2627,7 +2771,7 @@ def _set_cursor(self, enabled): else: cursor = backend_tools.Cursors.POINTER - self.ax.get_figure(root=True).canvas.set_cursor(cursor) + self._set_cursor(cursor) def connect_default_events(self): # docstring inherited @@ -2637,7 +2781,7 @@ def connect_default_events(self): def _press(self, event): """Button press event handler.""" - self._set_cursor(True) + self._set_span_cursor(enabled=True) if self._interactive and self._selection_artist.get_visible(): self._set_active_handle(event) else: @@ -2647,8 +2791,7 @@ def _press(self, event): # Clear previous rectangle before drawing new rectangle. self.update() - xdata, ydata = self._get_data_coords(event) - v = xdata if self.direction == 'horizontal' else ydata + v = event.xdata if self.direction == 'horizontal' else event.ydata if self._active_handle is None and not self.ignore_event_outside: # when the press event outside the span, we initially set the @@ -2685,9 +2828,10 @@ def direction(self, direction): else: self._direction = direction + @_call_with_reparented_event def _release(self, event): """Button release event handler.""" - self._set_cursor(False) + self._set_span_cursor(enabled=False) if not self._interactive: self._selection_artist.set_visible(False) @@ -2716,6 +2860,7 @@ def _release(self, event): return False + @_call_with_reparented_event def _hover(self, event): """Update the canvas cursor if it's over a handle.""" if self.ignore(event): @@ -2729,17 +2874,16 @@ def _hover(self, event): return _, e_dist = self._edge_handles.closest(event.x, event.y) - self._set_cursor(e_dist <= self.grab_range) + self._set_span_cursor(enabled=e_dist <= self.grab_range) def _onmove(self, event): """Motion notify event handler.""" - xdata, ydata = self._get_data_coords(event) if self.direction == 'horizontal': - v = xdata + v = event.xdata vpress = self._eventpress.xdata else: - v = ydata + v = event.ydata vpress = self._eventpress.ydata # move existing span @@ -3054,7 +3198,7 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent) (when already existing) or cancelled. minspany : float, default: 0 - Selections with an y-span less than or equal to *minspanx* are removed + Selections with a y-span less than or equal to *minspanx* are removed (when already existing) or cancelled. useblit : bool, default: False @@ -3120,6 +3264,13 @@ def onselect(eclick: MouseEvent, erelease: MouseEvent) """ +class _RectangleSelectorAction(enum.Enum): + ROTATE = enum.auto() + MOVE = enum.auto() + RESIZE = enum.auto() + CREATE = enum.auto() + + @_docstring.Substitution(_RECTANGLESELECTOR_PARAMETERS_DOCSTRING.replace( '__ARTIST_NAME__', 'rectangle')) class RectangleSelector(_SelectorWidget): @@ -3176,7 +3327,7 @@ def __init__(self, ax, onselect=None, *, minspanx=0, if props is None: props = dict(facecolor='red', edgecolor='black', alpha=0.2, fill=True) - props = {**props, 'animated': self.useblit} + props = {**props, 'animated': self._useblit} self._visible = props.pop('visible', self._visible) to_draw = self._init_shape(**props) self.ax.add_patch(to_draw) @@ -3201,18 +3352,18 @@ def __init__(self, ax, onselect=None, *, minspanx=0, xc, yc = self.corners self._corner_handles = ToolHandles(self.ax, xc, yc, marker_props=self._handle_props, - useblit=self.useblit) + useblit=self._useblit) self._edge_order = ['W', 'S', 'E', 'N'] xe, ye = self.edge_centers self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s', marker_props=self._handle_props, - useblit=self.useblit) + useblit=self._useblit) xc, yc = self.center self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s', marker_props=self._handle_props, - useblit=self.useblit) + useblit=self._useblit) self._active_handle = None @@ -3242,9 +3393,8 @@ def _press(self, event): if (self._active_handle is None and not self.ignore_event_outside and self._allow_creation): - x, y = self._get_data_coords(event) self._visible = False - self.extents = x, x, y, y + self.extents = event.xdata, event.xdata, event.ydata, event.ydata self._visible = True else: self.set_visible(True) @@ -3253,10 +3403,24 @@ def _press(self, event): self._rotation_on_press = self._rotation self._set_aspect_ratio_correction() + match self._get_action(): + case _RectangleSelectorAction.ROTATE: + # TODO: set to a rotate cursor if possible? + pass + case _RectangleSelectorAction.MOVE: + self._set_cursor(backend_tools.cursors.MOVE) + case _RectangleSelectorAction.RESIZE: + # TODO: set to a resize cursor if possible? + pass + case _RectangleSelectorAction.CREATE: + self._set_cursor(backend_tools.cursors.SELECT_REGION) + return False + @_call_with_reparented_event def _release(self, event): """Button release event handler.""" + self._set_cursor(backend_tools.Cursors.POINTER) if not self._interactive: self._selection_artist.set_visible(False) @@ -3300,9 +3464,20 @@ def _release(self, event): self.update() self._active_handle = None self._extents_on_press = None - return False + def _get_action(self): + state = self._state + if 'rotate' in state and self._active_handle in self._corner_order: + return _RectangleSelectorAction.ROTATE + elif self._active_handle == 'C': + return _RectangleSelectorAction.MOVE + elif self._active_handle: + return _RectangleSelectorAction.RESIZE + + return _RectangleSelectorAction.CREATE + + def _onmove(self, event): """ Motion notify event handler. @@ -3317,12 +3492,10 @@ def _onmove(self, event): # The calculations are done for rotation at zero: we apply inverse # transformation to events except when we rotate and move state = self._state - rotate = 'rotate' in state and self._active_handle in self._corner_order - move = self._active_handle == 'C' - resize = self._active_handle and not move + action = self._get_action() - xdata, ydata = self._get_data_coords(event) - if resize: + xdata, ydata = event.xdata, event.ydata + if action == _RectangleSelectorAction.RESIZE: inv_tr = self._get_rotation_transform().inverted() xdata, ydata = inv_tr.transform([xdata, ydata]) eventpress.xdata, eventpress.ydata = inv_tr.transform( @@ -3342,7 +3515,7 @@ def _onmove(self, event): x0, x1, y0, y1 = self._extents_on_press # rotate an existing shape - if rotate: + if action == _RectangleSelectorAction.ROTATE: # calculate angle abc a = (eventpress.xdata, eventpress.ydata) b = self.center @@ -3351,7 +3524,7 @@ def _onmove(self, event): np.arctan2(a[1]-b[1], a[0]-b[0])) self.rotation = np.rad2deg(self._rotation_on_press + angle) - elif resize: + elif action == _RectangleSelectorAction.RESIZE: size_on_press = [x1 - x0, y1 - y0] center = (x0 + size_on_press[0] / 2, y0 + size_on_press[1] / 2) @@ -3402,7 +3575,7 @@ def _onmove(self, event): sign = np.sign(xdata - x0) x1 = x0 + sign * abs(y1 - y0) * self._aspect_ratio_correction - elif move: + elif action == _RectangleSelectorAction.MOVE: x0, x1, y0, y1 = self._extents_on_press dx = xdata - eventpress.xdata dy = ydata - eventpress.ydata @@ -3697,7 +3870,7 @@ def __init__(self, ax, onselect=None, *, useblit=True, props=None, button=None): **(props if props is not None else {}), # Note that self.useblit may be != useblit, if the canvas doesn't # support blitting. - 'animated': self.useblit, 'visible': False, + 'animated': self._useblit, 'visible': False, } line = Line2D([], [], **props) self.ax.add_line(line) @@ -3707,6 +3880,7 @@ def _press(self, event): self.verts = [self._get_data(event)] self._selection_artist.set_visible(True) + @_call_with_reparented_event def _release(self, event): if self.verts is not None: self.verts.append(self._get_data(event)) @@ -3821,7 +3995,7 @@ def __init__(self, ax, onselect=None, *, useblit=False, if props is None: props = dict(color='k', linestyle='-', linewidth=2, alpha=0.5) - props = {**props, 'animated': self.useblit} + props = {**props, 'animated': self._useblit} self._selection_artist = line = Line2D([], [], **props) self.ax.add_line(line) @@ -3830,7 +4004,7 @@ def __init__(self, ax, onselect=None, *, useblit=False, markerfacecolor=props.get('color', 'k')) self._handle_props = handle_props self._polygon_handles = ToolHandles(self.ax, [], [], - useblit=self.useblit, + useblit=self._useblit, marker_props=self._handle_props) self._active_handle_idx = -1 @@ -3850,7 +4024,7 @@ def _get_bbox(self): def _add_box(self): self._box = RectangleSelector(self.ax, - useblit=self.useblit, + useblit=self._useblit, grab_range=self.grab_range, handle_props=self._box_handle_props, props=self._box_props, @@ -3877,6 +4051,7 @@ def _update_box(self): # Save a copy self._old_box_extents = self._box.extents + @_call_with_reparented_event def _scale_polygon(self, event): """ Scale the polygon selector points when the bounding box is moved or @@ -3941,6 +4116,7 @@ def _press(self, event): # support the 'move_all' state modifier). self._xys_at_press = self._xys.copy() + @_call_with_reparented_event def _release(self, event): """Button release event handler.""" # Release active tool handle. @@ -3960,11 +4136,12 @@ def _release(self, event): elif (not self._selection_completed and 'move_all' not in self._state and 'move_vertex' not in self._state): - self._xys.insert(-1, self._get_data_coords(event)) + self._xys.insert(-1, (event.xdata, event.ydata)) if self._selection_completed: self.onselect(self.verts) + @_call_with_reparented_event def onmove(self, event): """Cursor move event handler and validator.""" # Method overrides _SelectorWidget.onmove because the polygon selector @@ -3988,17 +4165,16 @@ def _onmove(self, event): # Move the active vertex (ToolHandle). if self._active_handle_idx >= 0: idx = self._active_handle_idx - self._xys[idx] = self._get_data_coords(event) + self._xys[idx] = (event.xdata, event.ydata) # Also update the end of the polygon line if the first vertex is # the active handle and the polygon is completed. if idx == 0 and self._selection_completed: - self._xys[-1] = self._get_data_coords(event) + self._xys[-1] = (event.xdata, event.ydata) # Move all vertices. elif 'move_all' in self._state and self._eventpress: - xdata, ydata = self._get_data_coords(event) - dx = xdata - self._eventpress.xdata - dy = ydata - self._eventpress.ydata + dx = event.xdata - self._eventpress.xdata + dy = event.ydata - self._eventpress.ydata for k in range(len(self._xys)): x_at_press, y_at_press = self._xys_at_press[k] self._xys[k] = x_at_press + dx, y_at_press + dy @@ -4018,7 +4194,7 @@ def _onmove(self, event): if len(self._xys) > 3 and v0_dist < self.grab_range: self._xys[-1] = self._xys[0] else: - self._xys[-1] = self._get_data_coords(event) + self._xys[-1] = (event.xdata, event.ydata) self._draw_polygon() @@ -4040,12 +4216,12 @@ def _on_key_release(self, event): and (event.key == self._state_modifier_keys.get('move_vertex') or event.key == self._state_modifier_keys.get('move_all'))): - self._xys.append(self._get_data_coords(event)) + self._xys.append((event.xdata, event.ydata)) self._draw_polygon() # Reset the polygon if the released key is the 'clear' key. elif event.key == self._state_modifier_keys.get('clear'): event = self._clean_event(event) - self._xys = [self._get_data_coords(event)] + self._xys = [(event.xdata, event.ydata)] self._selection_completed = False self._remove_box() self.set_visible(True) @@ -4130,7 +4306,7 @@ class Lasso(AxesWidget): def __init__(self, ax, xy, callback, *, useblit=True, props=None): super().__init__(ax) - self.useblit = useblit and self.canvas.supports_blit + self.useblit = useblit and self.canvas.supports_blit # TODO: Make dynamic if self.useblit: self.background = self.canvas.copy_from_bbox(self.ax.bbox) @@ -4147,24 +4323,26 @@ def __init__(self, ax, xy, callback, *, useblit=True, props=None): self.connect_event('button_release_event', self.onrelease) self.connect_event('motion_notify_event', self.onmove) + @_call_with_reparented_event def onrelease(self, event): if self.ignore(event): return if self.verts is not None: - self.verts.append(self._get_data_coords(event)) + self.verts.append((event.xdata, event.ydata)) if len(self.verts) > 2: self.callback(self.verts) self.line.remove() self.verts = None self.disconnect_events() + @_call_with_reparented_event def onmove(self, event): if (self.ignore(event) or self.verts is None or event.button != 1 or not self.ax.contains(event)[0]): return - self.verts.append(self._get_data_coords(event)) + self.verts.append((event.xdata, event.ydata)) self.line.set_data(list(zip(*self.verts))) if self.useblit: diff --git a/lib/matplotlib/widgets.pyi b/lib/matplotlib/widgets.pyi index 0fcd1990e17e..2f34255d625c 100644 --- a/lib/matplotlib/widgets.pyi +++ b/lib/matplotlib/widgets.pyi @@ -6,6 +6,7 @@ from .figure import Figure from .lines import Line2D from .patches import Polygon, Rectangle from .text import Text +from .backend_tools import Cursors import PIL.Image @@ -34,10 +35,12 @@ class Widget: class AxesWidget(Widget): ax: Axes def __init__(self, ax: Axes) -> None: ... + def __del__(self) -> None: ... @property def canvas(self) -> FigureCanvasBase | None: ... def connect_event(self, event: Event, callback: Callable) -> None: ... def disconnect_events(self) -> None: ... + def _set_cursor(self, cursor: Cursors) -> None: ... class Button(AxesWidget): label: Text @@ -64,7 +67,7 @@ class SliderBase(AxesWidget): valmax: float valstep: float | ArrayLike | None drag_active: bool - valfmt: str + valfmt: str | Callable[[float], str] | None def __init__( self, ax: Axes, @@ -73,7 +76,7 @@ class SliderBase(AxesWidget): closedmax: bool, valmin: float, valmax: float, - valfmt: str, + valfmt: str | Callable[[float], str] | None, dragging: Slider | None, valstep: float | ArrayLike | None, ) -> None: ... @@ -130,7 +133,7 @@ class RangeSlider(SliderBase): valmax: float, *, valinit: tuple[float, float] | None = ..., - valfmt: str | None = ..., + valfmt: str | Callable[[float], str] | None = ..., closedmin: bool = ..., closedmax: bool = ..., dragging: bool = ..., @@ -154,11 +157,11 @@ class CheckButtons(AxesWidget): actives: Iterable[bool] | None = ..., *, useblit: bool = ..., - label_props: dict[str, Any] | None = ..., + label_props: dict[str, Sequence[Any]] | None = ..., frame_props: dict[str, Any] | None = ..., check_props: dict[str, Any] | None = ..., ) -> None: ... - def set_label_props(self, props: dict[str, Any]) -> None: ... + def set_label_props(self, props: dict[str, Sequence[Any]]) -> None: ... def set_frame_props(self, props: dict[str, Any]) -> None: ... def set_check_props(self, props: dict[str, Any]) -> None: ... def set_active(self, index: int, state: bool | None = ...) -> None: ... # type: ignore[override] @@ -208,10 +211,10 @@ class RadioButtons(AxesWidget): activecolor: ColorType | None = ..., *, useblit: bool = ..., - label_props: dict[str, Any] | Sequence[dict[str, Any]] | None = ..., + label_props: dict[str, Sequence[Any]] | None = ..., radio_props: dict[str, Any] | None = ..., ) -> None: ... - def set_label_props(self, props: dict[str, Any]) -> None: ... + def set_label_props(self, props: dict[str, Sequence[Any]]) -> None: ... def set_radio_props(self, props: dict[str, Any]) -> None: ... def set_active(self, index: int) -> None: ... def clear(self) -> None: ... @@ -270,7 +273,7 @@ class MultiCursor(Widget): class _SelectorWidget(AxesWidget): onselect: Callable[[float, float], Any] - useblit: bool + _useblit: bool background: Any validButtons: list[MouseButton] def __init__( @@ -282,6 +285,8 @@ class _SelectorWidget(AxesWidget): state_modifier_keys: dict[str, str] | None = ..., use_data_coordinates: bool = ..., ) -> None: ... + @property + def useblit(self) -> bool: ... def update_background(self, event: Event) -> None: ... def connect_default_events(self) -> None: ... def ignore(self, event: Event) -> bool: ... @@ -335,6 +340,7 @@ class SpanSelector(_SelectorWidget): _props: dict[str, Any] | None = ..., _init: bool = ..., ) -> None: ... + def _set_span_cursor(self, *, enabled: bool) -> None: ... def connect_default_events(self) -> None: ... @property def direction(self) -> Literal["horizontal", "vertical"]: ... @@ -398,6 +404,7 @@ class RectangleSelector(_SelectorWidget): minspany: float spancoords: Literal["data", "pixels"] grab_range: float + _active_handle: None | Literal["C", "N", "NE", "E", "SE", "S", "SW", "W", "NW"] def __init__( self, ax: Axes, diff --git a/lib/mpl_toolkits/axes_grid1/axes_grid.py b/lib/mpl_toolkits/axes_grid1/axes_grid.py index 64bc8f465f19..b26c87edce1c 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_grid.py +++ b/lib/mpl_toolkits/axes_grid1/axes_grid.py @@ -84,8 +84,8 @@ def __init__(self, fig, ``121``), or as a `~.SubplotSpec`. nrows_ncols : (int, int) Number of rows and columns in the grid. - n_axes : int or None, default: None - If not None, only the first *n_axes* axes in the grid are created. + n_axes : int, optional + If given, only the first *n_axes* axes in the grid are created. direction : {"row", "column"}, default: "row" Whether axes are created in row-major ("row by row") or column-major order ("column by column"). This also affects the @@ -190,7 +190,7 @@ def _get_col_row(self, n): return col, row n_axes = property(lambda self: len(self.axes_all)) - ngrids = _api.deprecated(property(lambda self: len(self.axes_all))) + ngrids = _api.deprecated('3.11')(property(lambda self: len(self.axes_all))) # Good to propagate __len__ if we have __getitem__ def __len__(self): @@ -322,8 +322,8 @@ def __init__(self, fig, as a three-digit subplot position code (e.g., "121"). nrows_ncols : (int, int) Number of rows and columns in the grid. - n_axes : int or None, default: None - If not None, only the first *n_axes* axes in the grid are created. + n_axes : int, optional + If given, only the first *n_axes* axes in the grid are created. direction : {"row", "column"}, default: "row" Whether axes are created in row-major ("row by row") or column-major order ("column by column"). This also affects the @@ -364,7 +364,7 @@ def __init__(self, fig, cbar_set_cax : bool, default: True If True, each axes in the grid has a *cax* attribute that is bound to associated *cbar_axes*. - axes_class : subclass of `matplotlib.axes.Axes`, default: None + axes_class : subclass of `matplotlib.axes.Axes`, default: `.mpl_axes.Axes` """ _api.check_in_list(["each", "single", "edge", None], cbar_mode=cbar_mode) diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py index 52fe6efc0618..a1a9cc8df591 100644 --- a/lib/mpl_toolkits/axes_grid1/inset_locator.py +++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py @@ -341,11 +341,16 @@ def inset_axes(parent_axes, width, height, loc='upper right', %(Axes:kwdoc)s - borderpad : float, default: 0.5 + borderpad : float or (float, float), default: 0.5 Padding between inset axes and the bbox_to_anchor. + If a float, the same padding is used for both x and y. + If a tuple of two floats, it specifies the (x, y) padding. The units are axes font size, i.e. for a default font size of 10 points *borderpad = 0.5* is equivalent to a padding of 5 points. + .. versionadded:: 3.11 + The *borderpad* parameter now accepts a tuple of (x, y) paddings. + Returns ------- inset_axes : *axes_class* diff --git a/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/inset_locator.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/inset_locator.png index c7ad1e64b84d..17a1460f6be4 100644 Binary files a/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/inset_locator.png and b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/inset_locator.png 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 b6d72e408a52..f550dc9f531e 100644 --- a/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py +++ b/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py @@ -806,3 +806,5 @@ def test_grid_n_axes(): fig = plt.figure() grid = Grid(fig, 111, (3, 3), n_axes=5) assert len(fig.axes) == grid.n_axes == 5 + with pytest.warns(mpl.MatplotlibDeprecationWarning, match="ngrids attribute"): + assert grid.ngrids == 5 diff --git a/lib/mpl_toolkits/axisartist/axislines.py b/lib/mpl_toolkits/axisartist/axislines.py index c921ea597cb4..e0057d4f6c1e 100644 --- a/lib/mpl_toolkits/axisartist/axislines.py +++ b/lib/mpl_toolkits/axisartist/axislines.py @@ -16,7 +16,7 @@ In the new axes class, xaxis and yaxis is set to not visible by default, and new set of artist (AxisArtist) are defined to draw axis line, ticks, ticklabels and axis label. Axes.axis attribute serves as -a dictionary of these artists, i.e., ax.axis["left"] is a AxisArtist +a dictionary of these artists, i.e., ax.axis["left"] is an AxisArtist instance responsible to draw left y-axis. The default Axes.axis contains "bottom", "left", "top" and "right". diff --git a/lib/mpl_toolkits/axisartist/grid_finder.py b/lib/mpl_toolkits/axisartist/grid_finder.py index e51d4912c732..b984c18cab6c 100644 --- a/lib/mpl_toolkits/axisartist/grid_finder.py +++ b/lib/mpl_toolkits/axisartist/grid_finder.py @@ -169,13 +169,23 @@ def _format_ticks(self, idx, direction, factor, levels): return (fmt.format_ticks(levels) if isinstance(fmt, mticker.Formatter) else fmt(direction, factor, levels)) - def get_grid_info(self, x1, y1, x2, y2): + def get_grid_info(self, *args, **kwargs): """ - lon_values, lat_values : list of grid values. if integer is given, - rough number of grids in each direction. + Compute positioning information for grid lines and ticks, given the + axes' data *bbox*. """ + params = _api.select_matching_signature( + [lambda x1, y1, x2, y2: locals(), lambda bbox: locals()], *args, **kwargs) + if "x1" in params: + _api.warn_deprecated("3.11", message=( + "Passing extents as separate arguments to get_grid_info is deprecated " + "since %(since)s and support will be removed %(removal)s; pass a " + "single bbox instead.")) + bbox = Bbox.from_extents( + params["x1"], params["y1"], params["x2"], params["y2"]) + else: + bbox = params["bbox"] - bbox = Bbox.from_extents(x1, y1, x2, y2) tbbox = self.extreme_finder._find_transformed_bbox( self.get_transform().inverted(), bbox) diff --git a/lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py b/lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py index 1e27b3f571f3..aa37a3680fa5 100644 --- a/lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py +++ b/lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py @@ -341,7 +341,7 @@ def new_floating_axis(self, nth_coord, value, axes=None, axis_direction="bottom" return axisline def _update_grid(self, bbox): - self._grid_info = self.grid_finder.get_grid_info(*bbox.extents) + self._grid_info = self.grid_finder.get_grid_info(bbox) def get_gridlines(self, which="major", axis="both"): grid_lines = [] diff --git a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py index d44a61b6dd4a..96d8a2cde0f3 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py +++ b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py @@ -24,7 +24,8 @@ def test_ticks(): ax.add_artist(ticks_out) -@image_comparison(['axis_artist_labelbase.png'], style='default') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['axis_artist_labelbase.png'], style='default', tol=0.02) def test_labelbase(): # Remove this line when this test image is regenerated. plt.rcParams['text.kerning_factor'] = 6 @@ -41,7 +42,8 @@ def test_labelbase(): ax.add_artist(label) -@image_comparison(['axis_artist_ticklabels.png'], style='default') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['axis_artist_ticklabels.png'], style='default', tol=0.03) def test_ticklabels(): # Remove this line when this test image is regenerated. plt.rcParams['text.kerning_factor'] = 6 @@ -76,7 +78,8 @@ def test_ticklabels(): ax.set_ylim(0, 1) -@image_comparison(['axis_artist.png'], style='default') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['axis_artist.png'], style='default', tol=0.03) def test_axis_artist(): # Remove this line when this test image is regenerated. plt.rcParams['text.kerning_factor'] = 6 diff --git a/lib/mpl_toolkits/axisartist/tests/test_axislines.py b/lib/mpl_toolkits/axisartist/tests/test_axislines.py index a1485d4f436b..a47ab2ea8a31 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_axislines.py +++ b/lib/mpl_toolkits/axisartist/tests/test_axislines.py @@ -7,7 +7,8 @@ from mpl_toolkits.axisartist import Axes, SubplotHost -@image_comparison(['SubplotZero.png'], style='default') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['SubplotZero.png'], style='default', tol=0.02) def test_SubplotZero(): # Remove this line when this test image is regenerated. plt.rcParams['text.kerning_factor'] = 6 @@ -28,7 +29,8 @@ def test_SubplotZero(): ax.set_ylabel("Test") -@image_comparison(['Subplot.png'], style='default') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['Subplot.png'], style='default', tol=0.02) def test_Subplot(): # Remove this line when this test image is regenerated. plt.rcParams['text.kerning_factor'] = 6 @@ -130,7 +132,8 @@ def test_axisline_style_tight(): ax.axis[direction].set_visible(False) -@image_comparison(['subplotzero_ylabel.png'], style='mpl20') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['subplotzero_ylabel.png'], style='mpl20', tol=0.02) def test_subplotzero_ylabel(): fig = plt.figure() ax = fig.add_subplot(111, axes_class=SubplotZero) diff --git a/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py b/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py index 7d6554782fe6..ac31b8b30c97 100644 --- a/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py +++ b/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py @@ -76,7 +76,8 @@ def inverted(self): ax1.grid(True) -@image_comparison(['polar_box.png'], style='default', tol=0.04) +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['polar_box.png'], style='default', tol=0.09) def test_polar_box(): fig = plt.figure(figsize=(5, 5)) @@ -136,7 +137,7 @@ def test_polar_box(): # Remove tol & kerning_factor when this test image is regenerated. -@image_comparison(['axis_direction.png'], style='default', tol=0.13) +@image_comparison(['axis_direction.png'], style='default', tol=0.15) def test_axis_direction(): plt.rcParams['text.kerning_factor'] = 6 diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index 483fd09be163..d06d157db4ce 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -58,11 +58,11 @@ def get_dir_vector(zdir): x, y, z : array The direction vector. """ - if zdir == 'x': + if cbook._str_equal(zdir, 'x'): return np.array((1, 0, 0)) - elif zdir == 'y': + elif cbook._str_equal(zdir, 'y'): return np.array((0, 1, 0)) - elif zdir == 'z': + elif cbook._str_equal(zdir, 'z'): return np.array((0, 0, 1)) elif zdir is None: return np.array((0, 0, 0)) @@ -123,6 +123,16 @@ class Text3D(mtext.Text): def __init__(self, x=0, y=0, z=0, text='', zdir='z', axlim_clip=False, **kwargs): + if 'rotation' in kwargs: + _api.warn_external( + "The `rotation` parameter has not yet been implemented " + "and is currently ignored." + ) + if 'rotation_mode' in kwargs: + _api.warn_external( + "The `rotation_mode` parameter has not yet been implemented " + "and is currently ignored." + ) mtext.Text.__init__(self, x, y, text, **kwargs) self.set_3d_properties(z, zdir, axlim_clip) @@ -737,9 +747,8 @@ def set_depthshade( depthshade : bool Whether to shade the patches in order to give the appearance of depth. - depthshade_minalpha : float, default: None + depthshade_minalpha : float, default: :rc:`axes3d.depthshade_minalpha` Sets the minimum alpha value used by depth-shading. - If None, use the value from rcParams['axes3d.depthshade_minalpha']. .. versionadded:: 3.11 """ @@ -1112,17 +1121,15 @@ def patch_collection_2d_to_3d( zdir : {'x', 'y', 'z'} The axis in which to place the patches. Default: "z". See `.get_dir_vector` for a description of the values. - depthshade : bool, default: None + depthshade : bool, default: :rc:`axes3d.depthshade` Whether to shade the patches to give a sense of depth. - If None, use the value from rcParams['axes3d.depthshade']. axlim_clip : bool, default: False Whether to hide patches with a vertex outside the axes view limits. .. versionadded:: 3.10 - depthshade_minalpha : float, default: None + depthshade_minalpha : float, default: :rc:`axes3d.depthshade_minalpha` Sets the minimum alpha value used by depth-shading. - If None, use the value from rcParams['axes3d.depthshade_minalpha']. .. versionadded:: 3.11 """ diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 55b204022fb9..e8b72c421cd2 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -70,7 +70,7 @@ def __init__( ---------- fig : Figure The parent figure. - rect : tuple (left, bottom, width, height), default: None. + rect : tuple (left, bottom, width, height), default: (0, 0, 1, 1) The ``(left, bottom, width, height)`` Axes position. elev : float, default: 30 The elevation angle in degrees rotates the camera above and below @@ -244,6 +244,8 @@ def _transformed_cube(self, vals): (minx, maxy, maxz)] return proj3d._proj_points(xyzs, self.M) + @_api.delete_parameter("3.11", "share") + @_api.delete_parameter("3.11", "anchor") def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): """ Set the aspect ratios. @@ -263,39 +265,31 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): 'equalyz' adapt the y and z axes to have equal aspect ratios. ========= ================================================== - adjustable : None or {'box', 'datalim'}, optional - If not *None*, this defines which parameter will be adjusted to - meet the required aspect. See `.set_adjustable` for further - details. + adjustable : {'box', 'datalim'}, default: 'box' + Defines which parameter to adjust to meet the aspect ratio. + + - 'box': Change the physical dimensions of the axes bounding box. + - 'datalim': Change the x, y, or z data limits. anchor : None or str or 2-tuple of float, optional - If not *None*, this defines where the Axes will be drawn if there - is extra space due to aspect constraints. The most common way to - specify the anchor are abbreviations of cardinal directions: - - ===== ===================== - value description - ===== ===================== - 'C' centered - 'SW' lower left corner - 'S' middle of bottom edge - 'SE' lower right corner - etc. - ===== ===================== - - See `~.Axes.set_anchor` for further details. + .. deprecated:: 3.11 + This parameter has no effect. share : bool, default: False - If ``True``, apply the settings to all shared Axes. + .. deprecated:: 3.11 + This parameter has no effect. See Also -------- mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect """ + if adjustable is None: + adjustable = 'box' + _api.check_in_list(['box', 'datalim'], adjustable=adjustable) _api.check_in_list(('auto', 'equal', 'equalxy', 'equalyz', 'equalxz'), aspect=aspect) - super().set_aspect( - aspect='auto', adjustable=adjustable, anchor=anchor, share=share) + + self.set_adjustable(adjustable) self._aspect = aspect if aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'): @@ -1956,6 +1950,16 @@ def text(self, x, y, z, s, zdir=None, *, axlim_clip=False, **kwargs): `.Text3D` The created `.Text3D` instance. """ + if 'rotation' in kwargs: + _api.warn_external( + "The `rotation` parameter has not yet been implemented " + "and is currently ignored." + ) + if 'rotation_mode' in kwargs: + _api.warn_external( + "The `rotation_mode` parameter has not yet been implemented " + "and is currently ignored." + ) text = super().text(x, y, s, **kwargs) art3d.text_2d_to_3d(text, z, zdir, axlim_clip) return text @@ -2049,9 +2053,10 @@ def fill_between(self, x1, y1, z1, x2, y2, z2, *, - 'auto': If the points all lie on the same 3D plane, 'polygon' is used. Otherwise, 'quad' is used. - facecolors : list of :mpltype:`color`, default: None + facecolors : :mpltype:`color` or list of :mpltype:`color`, optional Colors of each individual patch, or a single color to be used for - all patches. + all patches. If not given, the next color from the patch color + cycle is used. shade : bool, default: None Whether to shade the facecolors. If *None*, then defaults to *True* @@ -2133,7 +2138,7 @@ def fill_between(self, x1, y1, z1, x2, y2, z2, *, polyc = art3d.Poly3DCollection(polys, facecolors=facecolors, shade=shade, axlim_clip=axlim_clip, **kwargs) - self.add_collection(polyc) + self.add_collection(polyc, autolim="_datalim_only") self.auto_scale_xyz([x1, x2], [y1, y2], [z1, z2], had_data) return polyc @@ -2332,7 +2337,7 @@ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, polys, facecolors=color, shade=shade, lightsource=lightsource, axlim_clip=axlim_clip, **kwargs) - self.add_collection(polyc) + self.add_collection(polyc, autolim="_datalim_only") self.auto_scale_xyz(X, Y, Z, had_data) return polyc @@ -2458,7 +2463,7 @@ def plot_wireframe(self, X, Y, Z, *, axlim_clip=False, **kwargs): lines = list(row_lines) + list(col_lines) linec = art3d.Line3DCollection(lines, axlim_clip=axlim_clip, **kwargs) - self.add_collection(linec) + self.add_collection(linec, autolim="_datalim_only") return linec @@ -2559,7 +2564,7 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, verts, *args, shade=shade, lightsource=lightsource, facecolors=color, axlim_clip=axlim_clip, **kwargs) - self.add_collection(polyc) + self.add_collection(polyc, autolim="_datalim_only") self.auto_scale_xyz(tri.x, tri.y, z, had_data) return polyc @@ -2889,8 +2894,10 @@ def add_collection3d(self, col, zs=0, zdir='z', autolim=True, *, if autolim: if isinstance(col, art3d.Line3DCollection): - self.auto_scale_xyz(*np.array(col._segments3d).transpose(), - had_data=had_data) + # Handle ragged arrays by extracting coordinates separately + all_points = np.concatenate(col._segments3d) + self.auto_scale_xyz(all_points[:, 0], all_points[:, 1], + all_points[:, 2], had_data=had_data) elif isinstance(col, art3d.Poly3DCollection): self.auto_scale_xyz(col._faces[..., 0], col._faces[..., 1], @@ -2901,7 +2908,7 @@ def add_collection3d(self, col, zs=0, zdir='z', autolim=True, *, # Currently unable to do so due to issues with Patch3DCollection # See https://github.com/matplotlib/matplotlib/issues/14298 for details - collection = super().add_collection(col) + collection = super().add_collection(col, autolim="_datalim_only") return collection @_preprocess_data(replace_names=["xs", "ys", "zs", "s", @@ -2943,15 +2950,13 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=None, - A 2D array in which the rows are RGB or RGBA. For more details see the *c* argument of `~.axes.Axes.scatter`. - depthshade : bool, default: None + depthshade : bool, default: :rc:`axes3d.depthshade` Whether to shade the scatter markers to give the appearance of depth. Each call to ``scatter()`` will perform its depthshading independently. - If None, use the value from rcParams['axes3d.depthshade']. - depthshade_minalpha : float, default: None + depthshade_minalpha : float, default: :rc:`axes3d.depthshade_minalpha` The lowest alpha value applied by depth-shading. - If None, use the value from rcParams['axes3d.depthshade_minalpha']. .. versionadded:: 3.11 @@ -3231,7 +3236,7 @@ def bar3d(self, x, y, z, dx, dy, dz, color=None, lightsource=lightsource, axlim_clip=axlim_clip, *args, **kwargs) - self.add_collection(col) + self.add_collection(col, autolim="_datalim_only") self.auto_scale_xyz((minx, maxx), (miny, maxy), (minz, maxz), had_data) @@ -3328,7 +3333,7 @@ def calc_arrows(UVW): if any(len(v) == 0 for v in input_args): # No quivers, so just make an empty collection and return early linec = art3d.Line3DCollection([], **kwargs) - self.add_collection(linec) + self.add_collection(linec, autolim="_datalim_only") return linec shaft_dt = np.array([0., length], dtype=float) @@ -3366,7 +3371,7 @@ def calc_arrows(UVW): lines = [] linec = art3d.Line3DCollection(lines, axlim_clip=axlim_clip, **kwargs) - self.add_collection(linec) + self.add_collection(linec, autolim="_datalim_only") self.auto_scale_xyz(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], had_data) @@ -3627,12 +3632,12 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', Use 'none' (case-insensitive) to plot errorbars without any data markers. - ecolor : :mpltype:`color`, default: None - The color of the errorbar lines. If None, use the color of the + ecolor : :mpltype:`color`, optional + The color of the errorbar lines. If not given, use the color of the line connecting the markers. - elinewidth : float, default: None - The linewidth of the errorbar lines. If None, the linewidth of + elinewidth : float, optional + The linewidth of the errorbar lines. If not given, the linewidth of the current style is used. capsize : float, default: :rc:`errorbar.capsize` @@ -3897,7 +3902,7 @@ def _extract_errs(err, data, lomask, himask): errline = art3d.Line3DCollection(np.array(coorderr).T, axlim_clip=axlim_clip, **eb_lines_style) - self.add_collection(errline) + self.add_collection(errline, autolim="_datalim_only") errlines.append(errline) coorderrs.append(coorderr) @@ -4047,7 +4052,7 @@ def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', stemlines = art3d.Line3DCollection( lines, linestyles=linestyle, colors=linecolor, label='_nolegend_', axlim_clip=axlim_clip) - self.add_collection(stemlines) + self.add_collection(stemlines, autolim="_datalim_only") markerline, = self.plot(x, y, z, markerfmt, label='_nolegend_') stem_container = StemContainer((markerline, stemlines, baseline), diff --git a/lib/mpl_toolkits/mplot3d/axis3d.py b/lib/mpl_toolkits/mplot3d/axis3d.py index 4da5031b990c..fdd22b717f67 100644 --- a/lib/mpl_toolkits/mplot3d/axis3d.py +++ b/lib/mpl_toolkits/mplot3d/axis3d.py @@ -708,6 +708,8 @@ def get_tightbbox(self, renderer=None, *, for_layout_only=False): bb_1, bb_2 = self._get_ticklabel_bboxes(ticks, renderer) other = [] + if self.offsetText.get_visible() and self.offsetText.get_text(): + other.append(self.offsetText.get_window_extent(renderer)) if self.line.get_visible(): other.append(self.line.get_window_extent(renderer)) if (self.label.get_visible() and not for_layout_only and diff --git a/lib/mpl_toolkits/mplot3d/tests/test_art3d.py b/lib/mpl_toolkits/mplot3d/tests/test_art3d.py index 174c12608ae9..aca943f9e0c0 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_art3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_art3d.py @@ -1,15 +1,32 @@ import numpy as np +import numpy.testing as nptest +import pytest import matplotlib.pyplot as plt from matplotlib.backend_bases import MouseEvent from mpl_toolkits.mplot3d.art3d import ( + get_dir_vector, Line3DCollection, Poly3DCollection, _all_points_on_plane, ) +@pytest.mark.parametrize("zdir, expected", [ + ("x", (1, 0, 0)), + ("y", (0, 1, 0)), + ("z", (0, 0, 1)), + (None, (0, 0, 0)), + ((1, 2, 3), (1, 2, 3)), + (np.array([4, 5, 6]), (4, 5, 6)), +]) +def test_get_dir_vector(zdir, expected): + res = get_dir_vector(zdir) + assert isinstance(res, np.ndarray) + nptest.assert_array_equal(res, expected) + + def test_scatter_3d_projection_conservation(): fig = plt.figure() ax = fig.add_subplot(projection='3d') @@ -55,7 +72,7 @@ def test_zordered_error(): fig = plt.figure() ax = fig.add_subplot(projection="3d") - ax.add_collection(Line3DCollection(lc)) + ax.add_collection(Line3DCollection(lc), autolim="_datalim_only") ax.scatter(*pc, visible=False) plt.draw() diff --git a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index cd45c8e33a6f..e9809ce2a106 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -3,6 +3,7 @@ import platform import sys +from packaging.version import parse as parse_version import pytest from mpl_toolkits.mplot3d import Axes3D, axes3d, proj3d, art3d @@ -13,11 +14,11 @@ from matplotlib import cm from matplotlib import colors as mcolors, patches as mpatch from matplotlib.testing.decorators import image_comparison, check_figures_equal -from matplotlib.testing.widgets import mock_event from matplotlib.collections import LineCollection, PolyCollection from matplotlib.patches import Circle, PathPatch from matplotlib.path import Path from matplotlib.text import Text +from matplotlib import _api import matplotlib.pyplot as plt import numpy as np @@ -181,7 +182,8 @@ def test_bar3d_shaded(): fig.canvas.draw() -@mpl3d_image_comparison(['bar3d_notshaded.png'], style='mpl20') +@mpl3d_image_comparison(['bar3d_notshaded.png'], style='mpl20', + tol=0.01 if parse_version(np.version.version).major < 2 else 0) def test_bar3d_notshaded(): fig = plt.figure() ax = fig.add_subplot(projection='3d') @@ -285,18 +287,11 @@ def test_contourf3d_extend(fig_test, fig_ref, extend, levels): # Z is in the range [0, 8] Z = X**2 + Y**2 - # Manually set the over/under colors to be the end of the colormap - cmap = mpl.colormaps['viridis'].copy() - cmap.set_under(cmap(0)) - cmap.set_over(cmap(255)) - # Set vmin/max to be the min/max values plotted on the reference image - kwargs = {'vmin': 1, 'vmax': 7, 'cmap': cmap} - ax_ref = fig_ref.add_subplot(projection='3d') - ax_ref.contourf(X, Y, Z, levels=[0, 2, 4, 6, 8], **kwargs) + ax_ref.contourf(X, Y, Z, levels=[0, 2, 4, 6, 8], vmin=1, vmax=7) ax_test = fig_test.add_subplot(projection='3d') - ax_test.contourf(X, Y, Z, levels, extend=extend, **kwargs) + ax_test.contourf(X, Y, Z, levels, extend=extend, vmin=1, vmax=7) for ax in [ax_ref, ax_test]: ax.set_xlim(-2, 2) @@ -652,7 +647,8 @@ def test_surface3d(): fig.colorbar(surf, shrink=0.5, aspect=5) -@image_comparison(['surface3d_label_offset_tick_position.png'], style='mpl20') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@image_comparison(['surface3d_label_offset_tick_position.png'], style='mpl20', tol=0.07) def test_surface3d_label_offset_tick_position(): plt.rcParams['axes3d.automargin'] = True # Remove when image is regenerated ax = plt.figure().add_subplot(projection="3d") @@ -748,7 +744,8 @@ def test_surface3d_masked_strides(): ax.view_init(60, -45, 0) -@mpl3d_image_comparison(['text3d.png'], remove_text=False, style='mpl20') +# TODO: tighten tolerance after baseline image is regenerated for text overhaul +@mpl3d_image_comparison(['text3d.png'], remove_text=False, style='mpl20', tol=0.1) def test_text3d(): fig = plt.figure() ax = fig.add_subplot(projection='3d') @@ -1127,8 +1124,9 @@ def test_poly3dCollection_autoscaling(): assert np.allclose(ax.get_zlim3d(), (-0.0833333333333333, 4.083333333333333)) +# TODO: tighten tolerance after baseline image is regenerated for text overhaul @mpl3d_image_comparison(['axes3d_labelpad.png'], - remove_text=False, style='mpl20') + remove_text=False, style='mpl20', tol=0.06) def test_axes3d_labelpad(): fig = plt.figure() ax = fig.add_axes(Axes3D(fig)) @@ -1218,7 +1216,7 @@ def _test_proj_draw_axes(M, s=1, *args, **kwargs): fig, ax = plt.subplots(*args, **kwargs) linec = LineCollection(lines) - ax.add_collection(linec) + ax.add_collection(linec, autolim="_datalim_only") for x, y, t in zip(txs, tys, ['o', 'x', 'y', 'z']): ax.text(x, y, t) @@ -2012,11 +2010,11 @@ def test_rotate(style): ax.figure.canvas.draw() # drag mouse to change orientation - ax._button_press( - mock_event(ax, button=MouseButton.LEFT, xdata=0, ydata=0)) - ax._on_move( - mock_event(ax, button=MouseButton.LEFT, - xdata=s*dx*ax._pseudo_w, ydata=s*dy*ax._pseudo_h)) + MouseEvent._from_ax_coords( + "button_press_event", ax, (0, 0), MouseButton.LEFT)._process() + MouseEvent._from_ax_coords( + "motion_notify_event", ax, (s*dx*ax._pseudo_w, s*dy*ax._pseudo_h), + MouseButton.LEFT)._process() ax.figure.canvas.draw() c = np.sqrt(3)/2 @@ -2076,10 +2074,10 @@ def convert_lim(dmin, dmax): z_center0, z_range0 = convert_lim(*ax.get_zlim3d()) # move mouse diagonally to pan along all axis. - ax._button_press( - mock_event(ax, button=MouseButton.MIDDLE, xdata=0, ydata=0)) - ax._on_move( - mock_event(ax, button=MouseButton.MIDDLE, xdata=1, ydata=1)) + MouseEvent._from_ax_coords( + "button_press_event", ax, (0, 0), MouseButton.MIDDLE)._process() + MouseEvent._from_ax_coords( + "motion_notify_event", ax, (1, 1), MouseButton.MIDDLE)._process() x_center, x_range = convert_lim(*ax.get_xlim3d()) y_center, y_range = convert_lim(*ax.get_ylim3d()) @@ -2553,11 +2551,10 @@ def test_on_move_vertical_axis(vertical_axis: str) -> None: ax.get_figure().canvas.draw() proj_before = ax.get_proj() - event_click = mock_event(ax, button=MouseButton.LEFT, xdata=0, ydata=1) - ax._button_press(event_click) - - event_move = mock_event(ax, button=MouseButton.LEFT, xdata=0.5, ydata=0.8) - ax._on_move(event_move) + MouseEvent._from_ax_coords( + "button_press_event", ax, (0, 1), MouseButton.LEFT)._process() + MouseEvent._from_ax_coords( + "motion_notify_event", ax, (.5, .8), MouseButton.LEFT)._process() assert ax._axis_names.index(vertical_axis) == ax._vertical_axis @@ -2691,3 +2688,100 @@ def test_ndarray_color_kwargs_value_error(): ax = fig.add_subplot(111, projection='3d') ax.scatter(1, 0, 0, color=np.array([0, 0, 0, 1])) fig.canvas.draw() + + +def test_line3dcollection_autolim_ragged(): + """Test Line3DCollection with autolim=True and lines of different lengths.""" + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + + # Create lines with different numbers of points (ragged arrays) + edges = [ + [(0, 0, 0), (1, 1, 1), (2, 2, 2)], # 3 points + [(0, 1, 0), (1, 2, 1)], # 2 points + [(1, 0, 1), (2, 1, 2), (3, 2, 3), (4, 3, 4)] # 4 points + ] + + # This should not raise an exception. + collections = ax.add_collection3d(art3d.Line3DCollection(edges), autolim=True) + + # Check that limits were computed correctly with margins + # The limits should include all points with default margins + assert np.allclose(ax.get_xlim3d(), (-0.08333333333333333, 4.083333333333333)) + assert np.allclose(ax.get_ylim3d(), (-0.0625, 3.0625)) + assert np.allclose(ax.get_zlim3d(), (-0.08333333333333333, 4.083333333333333)) + + +def test_axes3d_set_aspect_deperecated_params(): + """ + Test that using the deprecated 'anchor' and 'share' kwargs in + set_aspect raises the correct warning. + """ + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + + # Test that providing the `anchor` parameter raises a deprecation warning. + with pytest.warns(_api.MatplotlibDeprecationWarning, match="'anchor' parameter"): + ax.set_aspect('equal', anchor='C') + + # Test that using the 'share' parameter is now deprecated. + with pytest.warns(_api.MatplotlibDeprecationWarning, match="'share' parameter"): + ax.set_aspect('equal', share=True) + + # Test that the `adjustable` parameter is correctly processed to satisfy + # code coverage. + ax.set_aspect('equal', adjustable='box') + assert ax.get_adjustable() == 'box' + + ax.set_aspect('equal', adjustable='datalim') + assert ax.get_adjustable() == 'datalim' + + with pytest.raises(ValueError, match="adjustable"): + ax.set_aspect('equal', adjustable='invalid_value') + + +def test_axis_get_tightbbox_includes_offset_text(): + # Test that axis.get_tightbbox includes the offset_text + # Regression test for issue #30744 + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + + # Create data with high precision values that trigger offset text + Z = np.array([[0.1, 0.100000001], [0.100000000001, 0.100000000]]) + ny, nx = Z.shape + x = np.arange(nx) + y = np.arange(ny) + X, Y = np.meshgrid(x, y) + + ax.plot_surface(X, Y, Z) + + # Force a draw to ensure offset text is created and positioned + fig.canvas.draw() + renderer = fig.canvas.get_renderer() + + # Get the z-axis (which should have the offset text) + zaxis = ax.zaxis + + # Check that offset text is visible and has content + # The offset text may not be visible on all backends/configurations, + # so we only test the inclusion when it's actually present + if (zaxis.offsetText.get_visible() and + zaxis.offsetText.get_text()): + offset_bbox = zaxis.offsetText.get_window_extent(renderer) + + # Get the tight bbox - this should include the offset text + bbox = zaxis.get_tightbbox(renderer) + assert bbox is not None + assert offset_bbox is not None + + # The tight bbox should fully contain the offset text bbox + # Check that offset_bbox is within bbox bounds (with small tolerance for + # floating point errors) + assert bbox.x0 <= offset_bbox.x0 + 1e-6, \ + f"bbox.x0 ({bbox.x0}) should be <= offset_bbox.x0 ({offset_bbox.x0})" + assert bbox.y0 <= offset_bbox.y0 + 1e-6, \ + f"bbox.y0 ({bbox.y0}) should be <= offset_bbox.y0 ({offset_bbox.y0})" + assert bbox.x1 >= offset_bbox.x1 - 1e-6, \ + f"bbox.x1 ({bbox.x1}) should be >= offset_bbox.x1 ({offset_bbox.x1})" + assert bbox.y1 >= offset_bbox.y1 - 1e-6, \ + f"bbox.y1 ({bbox.y1}) should be >= offset_bbox.y1 ({offset_bbox.y1})" diff --git a/lib/mpl_toolkits/mplot3d/tests/test_legend3d.py b/lib/mpl_toolkits/mplot3d/tests/test_legend3d.py index 7fd676df1e31..9ca048e18ba9 100644 --- a/lib/mpl_toolkits/mplot3d/tests/test_legend3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_legend3d.py @@ -47,9 +47,9 @@ def test_linecollection_scaled_dashes(): lc3 = art3d.Line3DCollection(lines3, linestyles=":", lw=.5) fig, ax = plt.subplots(subplot_kw=dict(projection='3d')) - ax.add_collection(lc1) - ax.add_collection(lc2) - ax.add_collection(lc3) + ax.add_collection(lc1, autolim="_datalim_only") + ax.add_collection(lc2, autolim="_datalim_only") + ax.add_collection(lc3, autolim="_datalim_only") leg = ax.legend([lc1, lc2, lc3], ['line1', 'line2', 'line 3']) h1, h2, h3 = leg.legend_handles @@ -90,8 +90,7 @@ def test_contourf_legend_elements(): cs = ax.contourf(x, y, h, levels=[10, 30, 50], colors=['#FFFF00', '#FF00FF', '#00FFFF'], extend='both') - cs.cmap.set_over('red') - cs.cmap.set_under('blue') + cs.cmap = cs.cmap.with_extremes(over='red', under='blue') cs.changed() artists, labels = cs.legend_elements() assert labels == ['$x \\leq -1e+250s$', diff --git a/meson.build b/meson.build index 54249473fe8e..47244656705f 100644 --- a/meson.build +++ b/meson.build @@ -31,6 +31,10 @@ project( ], ) +# Enable bug fixes in Agg +add_project_arguments('-DMPL_FIX_AGG_IMAGE_FILTER_LUT_BUGS', language : 'cpp') +add_project_arguments('-DMPL_FIX_AGG_INTERPOLATION_ENDPOINT_BUG', language : 'cpp') + cc = meson.get_compiler('c') cpp = meson.get_compiler('cpp') diff --git a/pyproject.toml b/pyproject.toml index b580feff930e..b2e5451818f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ classifiers=[ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Scientific/Engineering :: Visualization", ] @@ -91,6 +92,7 @@ known_pydata = "numpy, matplotlib.pyplot" known_firstparty = "matplotlib,mpl_toolkits" sections = "FUTURE,STDLIB,THIRDPARTY,PYDATA,FIRSTPARTY,LOCALFOLDER" force_sort_within_sections = true +line_length = 88 [tool.ruff] extend-exclude = [ @@ -140,6 +142,7 @@ select = [ "E", "F", "W", + "UP035", # The following error codes require the preview mode to be enabled. "E201", "E202", diff --git a/requirements/doc/doc-requirements.txt b/requirements/doc/doc-requirements.txt index 77cb606130b0..1a352eaae975 100644 --- a/requirements/doc/doc-requirements.txt +++ b/requirements/doc/doc-requirements.txt @@ -14,8 +14,7 @@ ipywidgets ipykernel numpydoc>=1.0 packaging>=20 -pydata-sphinx-theme~=0.15.0 -mpl-sphinx-theme~=3.9.0 +mpl-sphinx-theme~=3.10.0 pyyaml PyStemmer sphinxcontrib-svg2pdfconverter>=1.1.0 diff --git a/requirements/testing/all.txt b/requirements/testing/all.txt index e386924a9b67..dd1dbf3f29fd 100644 --- a/requirements/testing/all.txt +++ b/requirements/testing/all.txt @@ -1,12 +1,12 @@ # pip requirements for all the CI builds -black<24 +black<26 certifi coverage!=6.3 psutil pytest!=4.6.0,!=5.4.0,!=8.1.0 pytest-cov -pytest-rerunfailures +pytest-rerunfailures!=16.0 pytest-timeout pytest-xdist pytest-xvfb diff --git a/src/_c_internal_utils.cpp b/src/_c_internal_utils.cpp index 0dddefaf32e3..31eb92444862 100644 --- a/src/_c_internal_utils.cpp +++ b/src/_c_internal_utils.cpp @@ -41,11 +41,11 @@ mpl_xdisplay_is_valid(void) // than dlopen(). if (getenv("DISPLAY") && (libX11 = dlopen("libX11.so.6", RTLD_LAZY))) { - typedef struct Display* (*XOpenDisplay_t)(char const*); - typedef int (*XCloseDisplay_t)(struct Display*); struct Display* display = nullptr; - XOpenDisplay_t XOpenDisplay = (XOpenDisplay_t)dlsym(libX11, "XOpenDisplay"); - XCloseDisplay_t XCloseDisplay = (XCloseDisplay_t)dlsym(libX11, "XCloseDisplay"); + auto XOpenDisplay = (struct Display* (*)(char const*)) + dlsym(libX11, "XOpenDisplay"); + auto XCloseDisplay = (int (*)(struct Display*)) + dlsym(libX11, "XCloseDisplay"); if (XOpenDisplay && XCloseDisplay && (display = XOpenDisplay(nullptr))) { XCloseDisplay(display); @@ -73,13 +73,11 @@ mpl_display_is_valid(void) void* libwayland_client; if (getenv("WAYLAND_DISPLAY") && (libwayland_client = dlopen("libwayland-client.so.0", RTLD_LAZY))) { - typedef struct wl_display* (*wl_display_connect_t)(char const*); - typedef void (*wl_display_disconnect_t)(struct wl_display*); struct wl_display* display = nullptr; - wl_display_connect_t wl_display_connect = - (wl_display_connect_t)dlsym(libwayland_client, "wl_display_connect"); - wl_display_disconnect_t wl_display_disconnect = - (wl_display_disconnect_t)dlsym(libwayland_client, "wl_display_disconnect"); + auto wl_display_connect = (struct wl_display* (*)(char const*)) + dlsym(libwayland_client, "wl_display_connect"); + auto wl_display_disconnect = (void (*)(struct wl_display*)) + dlsym(libwayland_client, "wl_display_disconnect"); if (wl_display_connect && wl_display_disconnect && (display = wl_display_connect(nullptr))) { wl_display_disconnect(display); @@ -162,25 +160,19 @@ mpl_SetProcessDpiAwareness_max(void) #ifdef _DPI_AWARENESS_CONTEXTS_ // These functions and options were added in later Windows 10 updates, so // must be loaded dynamically. - typedef BOOL (WINAPI *IsValidDpiAwarenessContext_t)(DPI_AWARENESS_CONTEXT); - typedef BOOL (WINAPI *SetProcessDpiAwarenessContext_t)(DPI_AWARENESS_CONTEXT); - HMODULE user32 = LoadLibrary("user32.dll"); - IsValidDpiAwarenessContext_t IsValidDpiAwarenessContextPtr = - (IsValidDpiAwarenessContext_t)GetProcAddress( - user32, "IsValidDpiAwarenessContext"); - SetProcessDpiAwarenessContext_t SetProcessDpiAwarenessContextPtr = - (SetProcessDpiAwarenessContext_t)GetProcAddress( - user32, "SetProcessDpiAwarenessContext"); + auto IsValidDpiAwarenessContext = (BOOL (WINAPI *)(DPI_AWARENESS_CONTEXT)) + GetProcAddress(user32, "IsValidDpiAwarenessContext"); + auto SetProcessDpiAwarenessContext = (BOOL (WINAPI *)(DPI_AWARENESS_CONTEXT)) + GetProcAddress(user32, "SetProcessDpiAwarenessContext"); DPI_AWARENESS_CONTEXT ctxs[3] = { DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, // Win10 Creators Update DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, // Win10 DPI_AWARENESS_CONTEXT_SYSTEM_AWARE}; // Win10 - if (IsValidDpiAwarenessContextPtr != NULL - && SetProcessDpiAwarenessContextPtr != NULL) { + if (IsValidDpiAwarenessContext && SetProcessDpiAwarenessContext) { for (size_t i = 0; i < sizeof(ctxs) / sizeof(DPI_AWARENESS_CONTEXT); ++i) { - if (IsValidDpiAwarenessContextPtr(ctxs[i])) { - SetProcessDpiAwarenessContextPtr(ctxs[i]); + if (IsValidDpiAwarenessContext(ctxs[i])) { + SetProcessDpiAwarenessContext(ctxs[i]); break; } } diff --git a/src/_image_resample.h b/src/_image_resample.h index 7e6c32c6bf64..1b7af133de31 100644 --- a/src/_image_resample.h +++ b/src/_image_resample.h @@ -496,7 +496,7 @@ typedef enum { } interpolation_e; -// T is rgba if and only if it has an T::r field. +// T is rgba if and only if it has a T::r field. template struct is_grayscale : std::true_type {}; template struct is_grayscale> : std::false_type {}; template constexpr bool is_grayscale_v = is_grayscale::value; @@ -569,23 +569,28 @@ class lookup_distortion { public: lookup_distortion(const double *mesh, int in_width, int in_height, - int out_width, int out_height) : + int out_width, int out_height, bool edge_aligned_subpixels) : m_mesh(mesh), m_in_width(in_width), m_in_height(in_height), m_out_width(out_width), - m_out_height(out_height) + m_out_height(out_height), + m_edge_aligned_subpixels(edge_aligned_subpixels) {} void calculate(int* x, int* y) { if (m_mesh) { + // Nearest-neighbor interpolation needs edge-aligned subpixels + // All other interpolation approaches need center-aligned subpixels + double offset = m_edge_aligned_subpixels ? 0 : 0.5; + double dx = double(*x) / agg::image_subpixel_scale; double dy = double(*y) / agg::image_subpixel_scale; if (dx >= 0 && dx < m_out_width && dy >= 0 && dy < m_out_height) { const double *coord = m_mesh + (int(dy) * m_out_width + int(dx)) * 2; - *x = int(coord[0] * agg::image_subpixel_scale); - *y = int(coord[1] * agg::image_subpixel_scale); + *x = int(coord[0] * agg::image_subpixel_scale + offset); + *y = int(coord[1] * agg::image_subpixel_scale + offset); } } } @@ -596,6 +601,7 @@ class lookup_distortion int m_in_height; int m_out_width; int m_out_height; + bool m_edge_aligned_subpixels; }; @@ -781,7 +787,7 @@ void resample( using span_conv_t = agg::span_converter; using nn_renderer_t = agg::renderer_scanline_aa; lookup_distortion dist( - params.transform_mesh, in_width, in_height, out_width, out_height); + params.transform_mesh, in_width, in_height, out_width, out_height, true); arbitrary_interpolator_t interpolator(inverted, dist); span_gen_t span_gen(input_accessor, interpolator); span_conv_t span_conv(span_gen, conv_alpha); @@ -806,7 +812,7 @@ void resample( using span_conv_t = agg::span_converter; using int_renderer_t = agg::renderer_scanline_aa; lookup_distortion dist( - params.transform_mesh, in_width, in_height, out_width, out_height); + params.transform_mesh, in_width, in_height, out_width, out_height, false); arbitrary_interpolator_t interpolator(inverted, dist); span_gen_t span_gen(input_accessor, interpolator, filter); span_conv_t span_conv(span_gen, conv_alpha); diff --git a/src/_image_wrapper.cpp b/src/_image_wrapper.cpp index 6528c4a9270c..c062ef14a8f1 100644 --- a/src/_image_wrapper.cpp +++ b/src/_image_wrapper.cpp @@ -167,12 +167,17 @@ image_resample(py::array input_array, if (is_affine) { convert_trans_affine(transform, params.affine); - params.is_affine = true; - } else { + // If affine parameters will make subpixels visible, treat as nonaffine instead + if (params.affine.sx >= agg::image_subpixel_scale / 2 || params.affine.sy >= agg::image_subpixel_scale / 2) { + is_affine = false; + params.affine = agg::trans_affine(); // reset to identity affine parameters + } + } + if (!is_affine) { transform_mesh = _get_transform_mesh(transform, output_array.shape()); params.transform_mesh = transform_mesh.data(); - params.is_affine = false; } + params.is_affine = is_affine; } if (auto resampler = diff --git a/src/_macosx.m b/src/_macosx.m index 1372157bc80d..9ca6c0749322 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -572,6 +572,8 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) }, }; +static PyTypeObject FigureManagerType; // forward declaration, needed in destroy() + typedef struct { PyObject_HEAD Window* window; @@ -580,6 +582,16 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) static PyObject* FigureManager_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { + if (![NSThread isMainThread]) { + PyErr_SetString( + PyExc_RuntimeError, + "Cannot create a GUI FigureManager outside the main thread " + "using the MacOS backend. Use a non-interactive " + "backend like 'agg' to make plots on worker threads." + ); + return NULL; + } + lazy_init(); Window* window = [Window alloc]; if (!window) { return NULL; } @@ -686,6 +698,25 @@ bool mpl_check_modifier(bool present, PyObject* list, char const* name) { [self->window close]; self->window = NULL; + + // call super(self, FigureManager).destroy() - it seems we need the + // explicit arguments, and just super() doesn't work in the C API. + PyObject *super_obj = PyObject_CallFunctionObjArgs( + (PyObject *)&PySuper_Type, + (PyObject *)&FigureManagerType, + self, + NULL + ); + if (super_obj == NULL) { + return NULL; // error + } + PyObject *result = PyObject_CallMethod(super_obj, "destroy", NULL); + Py_DECREF(super_obj); + if (result == NULL) { + return NULL; // error + } + Py_DECREF(result); + Py_RETURN_NONE; } @@ -1003,7 +1034,7 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } // Make it a zero-width box if we don't have enough room rect.size.width = fmax(bounds.size.width - rect.origin.x, 0); rect.origin.x = bounds.size.width - rect.size.width; - NSTextView* messagebox = [[[NSTextView alloc] initWithFrame: rect] autorelease]; + NSTextView* messagebox = [[NSTextView alloc] initWithFrame: rect]; messagebox.textContainer.maximumNumberOfLines = 2; messagebox.textContainer.lineBreakMode = NSLineBreakByTruncatingTail; messagebox.alignment = NSTextAlignmentRight; @@ -1013,7 +1044,6 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } /* if selectable, the messagebox can become first responder, * which is not supposed to happen */ [[window contentView] addSubview: messagebox]; - [messagebox release]; [[window contentView] display]; self->messagebox = messagebox; @@ -1024,6 +1054,7 @@ -(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } NavigationToolbar2_dealloc(NavigationToolbar2 *self) { [self->handler release]; + [self->messagebox release]; Py_TYPE(self)->tp_free((PyObject*)self); } diff --git a/src/_path.h b/src/_path.h index c03703776760..226d60231682 100644 --- a/src/_path.h +++ b/src/_path.h @@ -3,12 +3,12 @@ #ifndef MPL_PATH_H #define MPL_PATH_H -#include -#include -#include -#include #include +#include +#include +#include #include +#include #include "agg_conv_contour.h" #include "agg_conv_curve.h" @@ -26,6 +26,8 @@ struct XY double x; double y; + XY() : x(0), y(0) {} + XY(double x_, double y_) : x(x_), y(y_) { } @@ -43,7 +45,8 @@ struct XY typedef std::vector Polygon; -void _finalize_polygon(std::vector &result, int closed_only) +inline void +_finalize_polygon(std::vector &result, bool closed_only) { if (result.size() == 0) { return; @@ -311,43 +314,39 @@ inline bool point_on_path( struct extent_limits { - double x0; - double y0; - double x1; - double y1; - double xm; - double ym; -}; + XY start; + XY end; + /* minpos is the minimum positive values in the data; used by log scaling. */ + XY minpos; -void reset_limits(extent_limits &e) -{ - e.x0 = std::numeric_limits::infinity(); - e.y0 = std::numeric_limits::infinity(); - e.x1 = -std::numeric_limits::infinity(); - e.y1 = -std::numeric_limits::infinity(); - /* xm and ym are the minimum positive values in the data, used - by log scaling */ - e.xm = std::numeric_limits::infinity(); - e.ym = std::numeric_limits::infinity(); -} + extent_limits() : start{0,0}, end{0,0}, minpos{0,0} { + reset(); + } -inline void update_limits(double x, double y, extent_limits &e) -{ - if (x < e.x0) - e.x0 = x; - if (y < e.y0) - e.y0 = y; - if (x > e.x1) - e.x1 = x; - if (y > e.y1) - e.y1 = y; - /* xm and ym are the minimum positive values in the data, used - by log scaling */ - if (x > 0.0 && x < e.xm) - e.xm = x; - if (y > 0.0 && y < e.ym) - e.ym = y; -} + void reset() + { + start.x = std::numeric_limits::infinity(); + start.y = std::numeric_limits::infinity(); + end.x = -std::numeric_limits::infinity(); + end.y = -std::numeric_limits::infinity(); + minpos.x = std::numeric_limits::infinity(); + minpos.y = std::numeric_limits::infinity(); + } + + void update(double x, double y) + { + start.x = std::min(start.x, x); + start.y = std::min(start.y, y); + end.x = std::max(end.x, x); + end.y = std::max(end.y, y); + if (x > 0.0) { + minpos.x = std::min(minpos.x, x); + } + if (y > 0.0) { + minpos.y = std::min(minpos.y, y); + } + } +}; template void update_path_extents(PathIterator &path, agg::trans_affine &trans, extent_limits &extents) @@ -366,7 +365,7 @@ void update_path_extents(PathIterator &path, agg::trans_affine &trans, extent_li if ((code & agg::path_cmd_end_poly) == agg::path_cmd_end_poly) { continue; } - update_limits(x, y, extents); + extents.update(x, y); } } @@ -389,7 +388,7 @@ void get_path_collection_extents(agg::trans_affine &master_transform, agg::trans_affine trans; - reset_limits(extent); + extent.reset(); for (auto i = 0; i < N; ++i) { typename PathGenerator::path_iterator path(paths(i % Npaths)); @@ -524,12 +523,14 @@ struct bisectx { } - inline void bisect(double sx, double sy, double px, double py, double *bx, double *by) const + inline XY bisect(const XY s, const XY p) const { - *bx = m_x; - double dx = px - sx; - double dy = py - sy; - *by = sy + dy * ((m_x - sx) / dx); + double dx = p.x - s.x; + double dy = p.y - s.y; + return { + m_x, + s.y + dy * ((m_x - s.x) / dx), + }; } }; @@ -539,9 +540,9 @@ struct xlt : public bisectx { } - inline bool is_inside(double x, double y) const + inline bool is_inside(const XY point) const { - return x <= m_x; + return point.x <= m_x; } }; @@ -551,9 +552,9 @@ struct xgt : public bisectx { } - inline bool is_inside(double x, double y) const + inline bool is_inside(const XY point) const { - return x >= m_x; + return point.x >= m_x; } }; @@ -565,12 +566,14 @@ struct bisecty { } - inline void bisect(double sx, double sy, double px, double py, double *bx, double *by) const + inline XY bisect(const XY s, const XY p) const { - *by = m_y; - double dx = px - sx; - double dy = py - sy; - *bx = sx + dx * ((m_y - sy) / dy); + double dx = p.x - s.x; + double dy = p.y - s.y; + return { + s.x + dx * ((m_y - s.y) / dy), + m_y, + }; } }; @@ -580,9 +583,9 @@ struct ylt : public bisecty { } - inline bool is_inside(double x, double y) const + inline bool is_inside(const XY point) const { - return y <= m_y; + return point.y <= m_y; } }; @@ -592,9 +595,9 @@ struct ygt : public bisecty { } - inline bool is_inside(double x, double y) const + inline bool is_inside(const XY point) const { - return y >= m_y; + return point.y >= m_y; } }; } @@ -609,46 +612,30 @@ inline void clip_to_rect_one_step(const Polygon &polygon, Polygon &result, const return; } - auto [sx, sy] = polygon.back(); - for (auto [px, py] : polygon) { - sinside = filter.is_inside(sx, sy); - pinside = filter.is_inside(px, py); + auto s = polygon.back(); + for (auto p : polygon) { + sinside = filter.is_inside(s); + pinside = filter.is_inside(p); if (sinside ^ pinside) { - double bx, by; - filter.bisect(sx, sy, px, py, &bx, &by); - result.emplace_back(bx, by); + result.emplace_back(filter.bisect(s, p)); } if (pinside) { - result.emplace_back(px, py); + result.emplace_back(p); } - sx = px; - sy = py; + s = p; } } template -void -clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside, std::vector &results) +auto +clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside) { - double xmin, ymin, xmax, ymax; - if (rect.x1 < rect.x2) { - xmin = rect.x1; - xmax = rect.x2; - } else { - xmin = rect.x2; - xmax = rect.x1; - } - - if (rect.y1 < rect.y2) { - ymin = rect.y1; - ymax = rect.y2; - } else { - ymin = rect.y2; - ymax = rect.y1; - } + rect.normalize(); + auto xmin = rect.x1, xmax = rect.x2; + auto ymin = rect.y1, ymax = rect.y2; if (!inside) { std::swap(xmin, xmax); @@ -659,26 +646,27 @@ clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside, std::vecto curve_t curve(path); Polygon polygon1, polygon2; - double x = 0, y = 0; + XY point; unsigned code = 0; curve.rewind(0); + std::vector results; do { // Grab the next subpath and store it in polygon1 polygon1.clear(); do { if (code == agg::path_cmd_move_to) { - polygon1.emplace_back(x, y); + polygon1.emplace_back(point); } - code = curve.vertex(&x, &y); + code = curve.vertex(&point.x, &point.y); if (code == agg::path_cmd_stop) { break; } if (code != agg::path_cmd_move_to) { - polygon1.emplace_back(x, y); + polygon1.emplace_back(point); } } while ((code & agg::path_cmd_end_poly) != agg::path_cmd_end_poly); @@ -691,12 +679,14 @@ clip_path_to_rect(PathIterator &path, agg::rect_d &rect, bool inside, std::vecto // Empty polygons aren't very useful, so skip them if (polygon1.size()) { - _finalize_polygon(results, 1); + _finalize_polygon(results, true); results.push_back(polygon1); } } while (code != agg::path_cmd_stop); - _finalize_polygon(results, 1); + _finalize_polygon(results, true); + + return results; } template @@ -956,7 +946,7 @@ void convert_path_to_polygons(PathIterator &path, agg::trans_affine &trans, double width, double height, - int closed_only, + bool closed_only, std::vector &result) { typedef agg::conv_transform transformed_path_t; @@ -980,7 +970,7 @@ void convert_path_to_polygons(PathIterator &path, while ((code = curve.vertex(&x, &y)) != agg::path_cmd_stop) { if ((code & agg::path_cmd_end_poly) == agg::path_cmd_end_poly) { - _finalize_polygon(result, 1); + _finalize_polygon(result, true); polygon = &result.emplace_back(); } else { if (code == agg::path_cmd_move_to) { @@ -1051,15 +1041,14 @@ void cleanup_path(PathIterator &path, void quad2cubic(double x0, double y0, double x1, double y1, double x2, double y2, - double *outx, double *outy) + std::array &outx, std::array &outy) { - - outx[0] = x0 + 2./3. * (x1 - x0); - outy[0] = y0 + 2./3. * (y1 - y0); - outx[1] = outx[0] + 1./3. * (x2 - x0); - outy[1] = outy[0] + 1./3. * (y2 - y0); - outx[2] = x2; - outy[2] = y2; + std::get<0>(outx) = x0 + 2./3. * (x1 - x0); + std::get<0>(outy) = y0 + 2./3. * (y1 - y0); + std::get<1>(outx) = std::get<0>(outx) + 1./3. * (x2 - x0); + std::get<1>(outy) = std::get<0>(outy) + 1./3. * (y2 - y0); + std::get<2>(outx) = x2; + std::get<2>(outy) = y2; } @@ -1104,27 +1093,27 @@ void __add_number(double val, char format_code, int precision, template bool __convert_to_string(PathIterator &path, int precision, - char **codes, + const std::array &codes, bool postfix, std::string& buffer) { const char format_code = 'f'; - double x[3]; - double y[3]; + std::array x; + std::array y; double last_x = 0.0; double last_y = 0.0; unsigned code; - while ((code = path.vertex(&x[0], &y[0])) != agg::path_cmd_stop) { + while ((code = path.vertex(&std::get<0>(x), &std::get<0>(y))) != agg::path_cmd_stop) { if (code == CLOSEPOLY) { - buffer += codes[4]; + buffer += std::get<4>(codes); } else if (code < 5) { size_t size = NUM_VERTICES[code]; for (size_t i = 1; i < size; ++i) { - unsigned subcode = path.vertex(&x[i], &y[i]); + unsigned subcode = path.vertex(&x.at(i), &y.at(i)); if (subcode != code) { return false; } @@ -1133,29 +1122,29 @@ bool __convert_to_string(PathIterator &path, /* For formats that don't support quad curves, convert to cubic curves */ if (code == CURVE3 && codes[code - 1][0] == '\0') { - quad2cubic(last_x, last_y, x[0], y[0], x[1], y[1], x, y); + quad2cubic(last_x, last_y, x.at(0), y.at(0), x.at(1), y.at(1), x, y); code++; size = 3; } if (!postfix) { - buffer += codes[code - 1]; + buffer += codes.at(code - 1); buffer += ' '; } for (size_t i = 0; i < size; ++i) { - __add_number(x[i], format_code, precision, buffer); + __add_number(x.at(i), format_code, precision, buffer); buffer += ' '; - __add_number(y[i], format_code, precision, buffer); + __add_number(y.at(i), format_code, precision, buffer); buffer += ' '; } if (postfix) { - buffer += codes[code - 1]; + buffer += codes.at(code - 1); } - last_x = x[size - 1]; - last_y = y[size - 1]; + last_x = x.at(size - 1); + last_y = y.at(size - 1); } else { // Unknown code value return false; @@ -1174,7 +1163,7 @@ bool convert_to_string(PathIterator &path, bool simplify, SketchParams sketch_params, int precision, - char **codes, + const std::array &codes, bool postfix, std::string& buffer) { @@ -1211,7 +1200,6 @@ bool convert_to_string(PathIterator &path, sketch_t sketch(curve, sketch_params.scale, sketch_params.length, sketch_params.randomness); return __convert_to_string(sketch, precision, codes, postfix, buffer); } - } template diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index 2a297e49ac92..802189c428d3 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -68,15 +68,15 @@ Py_get_path_collection_extents(agg::trans_affine master_transform, py::ssize_t dims[] = { 2, 2 }; py::array_t extents(dims); - *extents.mutable_data(0, 0) = e.x0; - *extents.mutable_data(0, 1) = e.y0; - *extents.mutable_data(1, 0) = e.x1; - *extents.mutable_data(1, 1) = e.y1; + *extents.mutable_data(0, 0) = e.start.x; + *extents.mutable_data(0, 1) = e.start.y; + *extents.mutable_data(1, 0) = e.end.x; + *extents.mutable_data(1, 1) = e.end.y; py::ssize_t minposdims[] = { 2 }; py::array_t minpos(minposdims); - *minpos.mutable_data(0) = e.xm; - *minpos.mutable_data(1) = e.ym; + *minpos.mutable_data(0) = e.minpos.x; + *minpos.mutable_data(1) = e.minpos.y; return py::make_tuple(extents, minpos); } @@ -109,9 +109,7 @@ Py_path_in_path(mpl::PathIterator a, agg::trans_affine atrans, static py::list Py_clip_path_to_rect(mpl::PathIterator path, agg::rect_d rect, bool inside) { - std::vector result; - - clip_path_to_rect(path, rect, inside, result); + auto result = clip_path_to_rect(path, rect, inside); return convert_polygon_vector(result); } @@ -252,16 +250,11 @@ static py::object Py_convert_to_string(mpl::PathIterator path, agg::trans_affine trans, agg::rect_d cliprect, std::optional simplify, SketchParams sketch, int precision, - std::array codes_obj, bool postfix) + const std::array &codes, bool postfix) { - char *codes[5]; std::string buffer; bool status; - for (auto i = 0; i < 5; ++i) { - codes[i] = const_cast(codes_obj[i].c_str()); - } - if (!simplify.has_value()) { simplify = path.should_simplify(); } diff --git a/src/tri/_tri.h b/src/tri/_tri.h index 2319650b367b..994b1f43c556 100644 --- a/src/tri/_tri.h +++ b/src/tri/_tri.h @@ -75,7 +75,7 @@ namespace py = pybind11; -/* An edge of a triangle consisting of an triangle index in the range 0 to +/* An edge of a triangle consisting of a triangle index in the range 0 to * ntri-1 and an edge index in the range 0 to 2. Edge i goes from the * triangle's point i to point (i+1)%3. */ struct TriEdge final diff --git a/tools/boilerplate.py b/tools/boilerplate.py index 11ec15ac1c44..0a1a26c7cb76 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -1,12 +1,19 @@ """ Script to autogenerate pyplot wrappers. -When this script is run, the current contents of pyplot are -split into generatable and non-generatable content (via the magic header -:attr:`PYPLOT_MAGIC_HEADER`) and the generatable content is overwritten. -Hence, the non-generatable content should be edited in the pyplot.py file -itself, whereas the generatable content must be edited via templates in -this file. +pyplot.py consists of two parts: a hand-written part at the top, and an +automatically generated part at the bottom, starting with the comment + + ### REMAINING CONTENT GENERATED BY boilerplate.py ### + +This script generates the automatically generated part of pyplot.py. It +consists of colormap setter functions and wrapper functions for methods +of Figure and Axes. Whenever the API of one of the wrapped methods changes, +this script has to be rerun to keep pyplot.py up to date. + +The test ``lib/matplotlib/test_pyplot.py::test_pyplot_up_to_date`` checks +that the autogenerated part of pyplot.py is up to date. It will fail in the +case of an API mismatch and remind the developer to rerun this script. """ # Although it is possible to dynamically generate the pyplot functions at @@ -256,6 +263,7 @@ def boilerplate_gen(): 'pcolormesh', 'phase_spectrum', 'pie', + 'pie_label', 'plot', 'psd', 'quiver', diff --git a/tools/cache_zenodo_svg.py b/tools/cache_zenodo_svg.py index 3be7d6ca21e4..07b67a3e04ee 100644 --- a/tools/cache_zenodo_svg.py +++ b/tools/cache_zenodo_svg.py @@ -63,6 +63,11 @@ def _get_xdg_cache_dir(): if __name__ == "__main__": data = { + "v3.10.7": "17298696", + "v3.10.6": "16999430", + "v3.10.5": "16644850", + "v3.10.3": "15375714", + "v3.10.1": "14940554", "v3.10.0": "14464227", "v3.9.4": "14436121", "v3.9.3": "14249941", diff --git a/tools/stubtest.py b/tools/stubtest.py index b79ab2f40dd0..d73d966de19e 100644 --- a/tools/stubtest.py +++ b/tools/stubtest.py @@ -108,6 +108,7 @@ def visit_ClassDef(self, node): [ "stubtest", "--mypy-config-file=pyproject.toml", + "--ignore-disjoint-bases", "--allowlist=ci/mypy-stubtest-allowlist.txt", f"--allowlist={p}", "matplotlib", diff --git a/tools/triage_tests.py b/tools/triage_tests.py index 5153b1c712cb..6df720f29d2b 100644 --- a/tools/triage_tests.py +++ b/tools/triage_tests.py @@ -263,7 +263,7 @@ def __init__(self, path, root, source): ] self.thumbnails = [self.dir / x for x in self.thumbnails] - if not Path(self.destdir, self.generated).exists(): + if self.destdir is None or not Path(self.destdir, self.generated).exists(): # This case arises from a check_figures_equal test. self.status = 'autogen' elif ((self.dir / self.generated).read_bytes() @@ -281,7 +281,6 @@ def get_dest_dir(self, reldir): path = self.source / baseline_dir / reldir if path.is_dir(): return path - raise ValueError(f"Can't find baseline dir for {reldir}") @property def display(self):